Architecture
Design principles and data flow of the CourseKit timetable engine
Design Principles
CourseKit is built around five core principles:
Pluggable Storage
All database operations go through abstract storage classes (TimetableEventStorage, RoomStorage, etc.). This means you can use any database by implementing the storage interfaces. Prisma adapters are included out of the box.
Materialization Pattern
Recurring events are stored as a single TimetableEvent with an RRULE string. At query time, RecurrenceService.materialize() expands them into concrete MaterializedOccurrence objects for a given date range, applying exceptions (cancellations, modifications, additions).
Constraint Pipeline
Conflict detection runs all registered ScheduleConstraint implementations against materialized occurrences. Three constraints are built-in (overlap, capacity, availability), and you can add custom ones. The ConflictService.dryRun() method lets you preview conflicts before committing changes.
Domain Events
All state-changing operations emit events via @nestjs/event-emitter. Subscribe to DOMAIN_EVENTS.EVENT_CREATED, DOMAIN_EVENTS.CONFLICT_DETECTED, and others to build reactive integrations (notifications, audit logs, sync).
Structural Typing
The backend package never imports @prisma/client. All entity types are defined as plain TypeScript interfaces. The Prisma adapters map between Prisma models and these structural types, keeping your domain logic ORM-agnostic.
Data Model
TimetableEvent (a scheduled occurrence or series)
├── recurrenceRule: RRULE string (null = one-time)
├── EventException[] (cancel, modify, or add occurrences)
├── EventInstructor[] (many-to-many with role)
├── EventGroup[] (many-to-many)
├── Room? (assigned room)
├── Course? (parent course)
└── AcademicPeriod? (semester/holiday/exam)
Instructor
├── Availability[] (when they can/can't teach)
└── EventInstructor[] (assigned events)
Room
├── capacity, building, campus
├── Availability[] (when the room is free/blocked)
└── TimetableEvent[] (assigned events)
Group
├── type: 'fixed' | 'enrollment'
├── StudentGroup[] → Student[]
└── EventGroup[] (assigned events)
Course (hierarchical)
├── code, metadata
├── parent/children (self-referencing)
└── TimetableEvent[]
AcademicPeriod (hierarchical)
├── type: 'semester' | 'holiday' | 'exam' | 'break'
├── startDate / endDate
└── parent/children (self-referencing)
LocationDistance
└── fromCampus → toCampus with travelMinutesData Flow
Event Creation
- Create a
TimetableEventwith title, start time, duration, and optional RRULE - Assign instructors via
EventInstructorjoin records - Assign groups via
EventGroupjoin records - Optionally link to a
Room,Course, andAcademicPeriod DOMAIN_EVENTS.EVENT_CREATEDis emitted
Schedule Query
QueryService.getSchedule()receives aScheduleQuerywith date range and optional filtersTimetableEventStorage.findByQuery()fetches matching events from storage- For each event,
RecurrenceService.materialize()expands RRULE + exceptions intoMaterializedOccurrence[] - Occurrences are sorted by start time and returned
Conflict Detection
ConflictService.check()materializes all events in a date range- Each registered
ScheduleConstraintevaluates the full set of occurrences - Built-in constraints check for:
- Overlap: instructor double-bookings and room conflicts
- Capacity: room capacity vs. total student count
- Availability: events scheduled during blocked time windows
- All violations are collected into a
ConflictCheckResult DOMAIN_EVENTS.CONFLICT_DETECTEDis emitted if conflicts exist
Dry-Run Preview
ConflictService.dryRun()creates a temporary event (not persisted)- The temporary event is added to the existing schedule
- The full constraint pipeline runs against the combined set
- Conflicts are returned without side effects
Exception Handling
Recurring Event (RRULE)
│
├── Normal occurrence (materialized from RRULE)
├── Cancelled exception (EXDATE — occurrence removed)
├── Modified exception (occurrence with different time/room)
└── Added exception (extra occurrence not in RRULE)Exceptions are stored as EventException records linked to the parent event. During materialization, cancelled dates are excluded, modified dates use override values, and added dates create extra occurrences.