My App
CourseKit

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 travelMinutes

Data Flow

Event Creation

  1. Create a TimetableEvent with title, start time, duration, and optional RRULE
  2. Assign instructors via EventInstructor join records
  3. Assign groups via EventGroup join records
  4. Optionally link to a Room, Course, and AcademicPeriod
  5. DOMAIN_EVENTS.EVENT_CREATED is emitted

Schedule Query

  1. QueryService.getSchedule() receives a ScheduleQuery with date range and optional filters
  2. TimetableEventStorage.findByQuery() fetches matching events from storage
  3. For each event, RecurrenceService.materialize() expands RRULE + exceptions into MaterializedOccurrence[]
  4. Occurrences are sorted by start time and returned

Conflict Detection

  1. ConflictService.check() materializes all events in a date range
  2. Each registered ScheduleConstraint evaluates the full set of occurrences
  3. 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
  4. All violations are collected into a ConflictCheckResult
  5. DOMAIN_EVENTS.CONFLICT_DETECTED is emitted if conflicts exist

Dry-Run Preview

  1. ConflictService.dryRun() creates a temporary event (not persisted)
  2. The temporary event is added to the existing schedule
  3. The full constraint pipeline runs against the combined set
  4. 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.

On this page