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 trackingData Flow
Booking Creation
BookingService.create()validates the DTO and checks idempotency- Priority is resolved: explicit value wins, otherwise derived from
purposeTypeviaPriorityService - The target room is verified to exist
BookingStorage.createWithConflictCheck()atomically creates the booking and checks for overlapping bookings (direct + partition-level)BookingRequestedevent is emitted
Availability Search
AvailabilityService.search()queries candidate rooms by capacity, equipment, accessibility, and location scope- Each candidate is checked for overlapping bookings and active blackout windows
- Surviving rooms are scored by capacity fit and equipment match
- Results are sorted by score descending with cursor-based pagination
Conflict Detection
ConflictService.checkConflicts()finds direct overlapping bookings in the target room- The room's partition tree is loaded (parent/child relationships)
- Each related room in the partition tree is also checked for overlaps
- A
ConflictCheckResultis returned withdirectConflictsandpartitionConflicts
Conflict Alternatives
ConflictService.suggestAlternatives()scans forward in 30-minute increments for same-room-different-time slots (up to 7 days)- Different-room-same-time candidates are found via compound filter and scored by capacity similarity
- Suggestions are sorted: same-room-different-time first (score 1.0), then by score descending
Recurrence Expansion
RecurrenceService.expandSeries()generates concrete dates from a rule definition- WEEKLY: every week on specified days. BIWEEKLY: every other week. CUSTOM: only specified ISO calendar weeks
- Exception dates are excluded
createRecurringBooking()creates individual bookings for each expanded date, recording conflicts without aborting
Blackout Impact
BlackoutService.create()persists a blackout window at a location nodeanalyzeImpact()resolves all descendant rooms from the blackout's location node- Each descendant room is checked for overlapping bookings
BlackoutImpactDetectedevent is emitted with the list of affected bookings
State Transitions
requested ──→ confirmed ──→ in_progress ──→ completed
│ │ │
└──────────────┴──────────────┴──→ cancelledTerminal states (completed, cancelled) allow no further transitions. Each transition is recorded as a BookingStateTransition for audit purposes.