My App
RoomKit

Architecture

Design principles and data flow of the RoomKit booking engine

Design Principles

RoomKit is built around six core principles:

Pluggable Storage

All database operations go through abstract storage classes (BookingStorage, RoomStorage, etc.). You can use any database by implementing the storage interfaces. A composite PrismaRoomKitAdapter is included for Prisma out of the box.

Booking State Machine

Every booking follows a strict lifecycle: requested → confirmed → in_progress → completed. Cancellation is allowed from any non-terminal state. The BookingStateMachine validates transitions and prevents invalid state changes.

Event-Driven

All state-changing operations emit typed events via the EventBus. Subscribe to BookingRequested, ConflictDetected, BlackoutCreated, and 9 other event types to build reactive integrations (notifications, audit logs, sync).

Cascading Configuration

Config values are set at any level of the location hierarchy (global, institution, campus, building, etc.). The ConfigService resolves values by walking up the tree from the target node, with more specific values overriding less specific ones.

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.

Partition-Aware Conflicts

Room partitions (parent/child relationships) are checked during conflict detection. Booking a parent room blocks all child rooms, and vice versa, preventing double-bookings across divisible spaces.

Data Model

LocationNode (hierarchical tree)
  ├── type: institution | campus | building | floor | wing | room
  ├── path: unique slug path (e.g., "hfu/campus-a/building-1/floor-2/room-201")
  ├── aliases: alternative names
  ├── children: LocationNode[]
  ├── OperatingHours[] (day-of-week + open/close times)
  ├── BlackoutWindow[] (scoped time blocks)
  └── ConfigEntry[] (inheritable key-value settings)

Room
  ├── locationNodeId → LocationNode
  ├── seatedCapacity, examCapacity, standingCapacity
  ├── setupBufferMinutes, teardownBufferMinutes
  ├── RoomEquipment[] (projector, whiteboard, etc.)
  ├── RoomAccessibility[] (wheelchair, hearing_loop, etc.)
  ├── RoomPartition[] (parent/child room relationships)
  └── Booking[]

Booking
  ├── roomId → Room
  ├── requesterId, onBehalfOfId
  ├── startsAt, endsAt
  ├── status: requested | confirmed | in_progress | completed | cancelled
  ├── priority (auto-resolved from purposeType)
  ├── version (optimistic concurrency)
  ├── idempotencyKey (deduplication)
  ├── RecurrenceRule? (linked series)
  ├── recurrenceModType: original | modified | detached
  ├── BookingStateTransition[] (audit trail)
  ├── ExamSession? (exam mode data)
  └── ConflictRecord[] (conflict history)

RecurrenceRule
  ├── frequency: weekly | biweekly | custom
  ├── daysOfWeek: number[] (0=Sunday)
  ├── calendarWeeks: number[] (ISO week numbers, for custom)
  ├── seriesStartsAt, seriesEndsAt
  ├── exceptionDates: Date[]
  └── Booking[] (expanded instances)

PriorityTier
  └── name + weight (LECTURE=100, SEMINAR=75, etc.)

BulkOperation
  └── type + status + progress tracking

Data Flow

Booking Creation

  1. BookingService.create() validates the DTO and checks idempotency
  2. Priority is resolved: explicit value wins, otherwise derived from purposeType via PriorityService
  3. The target room is verified to exist
  4. BookingStorage.createWithConflictCheck() atomically creates the booking and checks for overlapping bookings (direct + partition-level)
  5. BookingRequested event is emitted
  1. AvailabilityService.search() queries candidate rooms by capacity, equipment, accessibility, and location scope
  2. Each candidate is checked for overlapping bookings and active blackout windows
  3. Surviving rooms are scored by capacity fit and equipment match
  4. Results are sorted by score descending with cursor-based pagination

Conflict Detection

  1. ConflictService.checkConflicts() finds direct overlapping bookings in the target room
  2. The room's partition tree is loaded (parent/child relationships)
  3. Each related room in the partition tree is also checked for overlaps
  4. A ConflictCheckResult is returned with directConflicts and partitionConflicts

Conflict Alternatives

  1. ConflictService.suggestAlternatives() scans forward in 30-minute increments for same-room-different-time slots (up to 7 days)
  2. Different-room-same-time candidates are found via compound filter and scored by capacity similarity
  3. Suggestions are sorted: same-room-different-time first (score 1.0), then by score descending

Recurrence Expansion

  1. RecurrenceService.expandSeries() generates concrete dates from a rule definition
  2. WEEKLY: every week on specified days. BIWEEKLY: every other week. CUSTOM: only specified ISO calendar weeks
  3. Exception dates are excluded
  4. createRecurringBooking() creates individual bookings for each expanded date, recording conflicts without aborting

Blackout Impact

  1. BlackoutService.create() persists a blackout window at a location node
  2. analyzeImpact() resolves all descendant rooms from the blackout's location node
  3. Each descendant room is checked for overlapping bookings
  4. BlackoutImpactDetected event is emitted with the list of affected bookings

State Transitions

requested ──→ confirmed ──→ in_progress ──→ completed
    │              │              │
    └──────────────┴──────────────┴──→ cancelled

Terminal states (completed, cancelled) allow no further transitions. Each transition is recorded as a BookingStateTransition for audit purposes.

On this page