Fundamentally refactors booking system to support: - Split-resource bookings - Service-level resource assignment - Clear separation between booking and calendar events Introduces new interfaces and type definitions to model complex booking scenarios, enabling more flexible and accurate resource management Improves type safety by removing ambiguous type annotations
16 KiB
Booking & Resource Architecture Redesign
Date: 2025-11-14
Duration: ~2+ hours
Initial Scope: Create IResource interface and fix Date | any type
Actual Scope: Fundamental redesign of booking architecture with service-level resource assignment
Executive Summary
What started as a simple task to create an IResource interface evolved into a comprehensive architectural redesign after critical developer questioning revealed fundamental flaws in the initial approach. Through 4 major iterations and multiple course corrections, we arrived at a clean architecture that properly models split-resource bookings (e.g., student + master stylist working on the same booking).
Key Achievement: Avoided building 3 different incorrect architectures by thinking through real business scenarios before committing to code.
Original Request
"Can we create an IResource now? Then you can also fix
data: Date | any"
Expected: 10 minutes - create interface, update type, done.
Reality: 2+ hours of architectural discussion, 4 major iterations, 930 lines of documentation.
Iteration History
Iteration 1: "Booking HAS resourceId (Primary Resource)" ❌
My Initial Assumption:
export interface IBooking {
id: string;
resourceId: string; // Primary resource
customerId?: string;
type: BookingType;
services?: IBookingService[];
}
Rationale: Bookings are always resource-bound (someone performs the service), so booking should have a resourceId.
Developer Challenge:
"A booking with 2 services can be split between multiple resources. For example, a large hair coloring - the student handles hair wash and eyebrows, the master stylist does the actual coloring."
Problem Identified:
- Booking.resourceId assumes ONE primary resource
- But in reality: hårvask (student) + bundfarve (master) = TWO resources, neither "primary"
- Model doesn't support split-resource bookings
Result: ❌ Discarded after 15 minutes of discussion
Iteration 2: "Booking HAS Primary + Services Have Resources" ❌
My Second Attempt:
export interface IBooking {
id: string;
resourceId: string; // "Primary" responsible person
customerId?: string;
services?: IBookingService[];
}
export interface IBookingService {
serviceId: string;
resourceId: string; // Who performs THIS service
}
Rationale: Keep primary resource for "who is responsible" while allowing service-level assignment.
Developer Challenge:
"Why should there be a primary? Use case: Customer chooses service, UI shows it can be split between Karina and Nanna, customer says yes. Here there's no primary, both are master stylists."
Problem Identified:
- "Primary resource" is an artificial concept
- Equal splits (Karina + Nanna) don't have a "responsible person"
- Creates confusion: "Who is primary if they're equal?"
Result: ❌ Discarded after thorough use case review
Iteration 3: "Booking WITHOUT resourceId, Only Services Have Resources" ✅
My Third Attempt:
export interface IBooking {
id: string;
customerId: string; // REQUIRED
services: IBookingService[]; // REQUIRED
// NO resourceId at all!
}
export interface IBookingService {
serviceId: string;
resourceId: string; // Resource performs THIS service
}
My Concern:
"But what about vacation/break? Those are resource-bound but have no services!"
Developer Response:
"Vacation is not a booking, it's just a CalendarEvent with resourceId. Bookings are only for customers. We always have a booking for customer services."
Breakthrough Realization:
- Booking = Customer + Services (business domain)
- CalendarEvent = Scheduling + Resource (time domain)
- Vacation/break/meeting are CalendarEvents WITHOUT bookings
Architecture Clarified:
// Booking = ONLY customer services
export interface IBooking {
customerId: string; // REQUIRED - always customer
services: IBookingService[]; // REQUIRED - always has services
// NO resourceId
// NO type (always customer)
}
// CalendarEvent = ALL scheduling
export interface ICalendarEvent {
type: CalendarEventType; // 'customer' | 'vacation' | 'break' | ...
bookingId?: string; // Only if type = 'customer'
resourceId?: string; // Which resource owns this slot
}
Result: ✅ ACCEPTED - Clean separation of concerns
Iteration 4: "BookingType Should Be Removed" ✅
My Miss:
I created BookingType union even though IBooking no longer had a type field.
Developer Catch:
"But wait... booking type is gone, right? It's always customer-related."
Fix Applied:
- Renamed
BookingType→CalendarEventType - Clarified: Only
ICalendarEventuses this type, notIBooking - Updated all references
Result: ✅ Type system now consistent
Key Architectural Decisions
Decision 1: Booking = Customer ONLY
Before: Booking could be customer, vacation, break, meeting After: Booking is ONLY for customer services
// ✅ Valid
{
"customerId": "CUST456",
"services": [{ "resourceId": "EMP001" }]
}
// ❌ Invalid - vacation is NOT a booking
{
"type": "vacation",
"resourceId": "EMP001"
}
Vacation is now:
{
"type": "vacation",
"bookingId": null, // NO booking
"resourceId": "EMP001" // Just a time slot
}
Decision 2: Resources Assigned Per Service
Before: Booking.resourceId (primary) After: IBookingService.resourceId (per service)
Enables:
- Split resources: student + master
- Equal splits: Karina + Nanna
- Resource reassignment: "student is sick, assign to another student"
Example - Split Resource Booking:
{
"customerId": "CUST456",
"services": [
{
"serviceName": "Hårvask",
"resourceId": "STUDENT001",
"baseDuration": 30
},
{
"serviceName": "Bundfarve",
"resourceId": "EMP001",
"baseDuration": 90
}
]
}
Two CalendarEvents created:
- Event 1: Student's calendar, 13:00-13:30
- Event 2: Master's calendar, 13:30-15:00
- Both reference same booking
Decision 3: 1 Booking → N Events
Relationship: One booking can span multiple events
Scenarios:
- Split Session: 180min service split across 2 days (same resource)
- Split Resources: Student + Master (different resources)
- Equal Split: Karina + Nanna (two masters, equal)
Key Insight: Events are scheduled per resource, not per booking.
Decision 4: CalendarEvent.type Determines Booking Existence
type CalendarEventType =
| 'customer' // HAS booking
| 'vacation' // NO booking
| 'break' // NO booking
| 'meeting' // NO booking
| 'blocked'; // NO booking
Business Rule: If event.type === 'customer', then event.bookingId must be set.
Database Schema Changes
Before (Iteration 1 - Wrong)
CREATE TABLE Booking (
ResourceId VARCHAR(50) NOT NULL, -- ❌ Wrong: assumes one resource
CustomerId VARCHAR(50) NULL, -- ❌ Wrong: should be required
Type VARCHAR(20) NOT NULL -- ❌ Wrong: always customer
);
CREATE TABLE BookingService (
BookingId VARCHAR(50) NOT NULL,
ServiceId VARCHAR(50) NOT NULL
-- ❌ Missing: ResourceId
);
After (Final Design - Correct)
CREATE TABLE Booking (
Id VARCHAR(50) PRIMARY KEY,
CustomerId VARCHAR(50) NOT NULL, -- ✅ REQUIRED
Status VARCHAR(20) NOT NULL,
TotalPrice DECIMAL(10,2) NULL,
-- ✅ NO ResourceId (moved to service level)
-- ✅ NO Type (always customer)
FOREIGN KEY (CustomerId) REFERENCES Customer(Id)
);
CREATE TABLE BookingService (
BookingId VARCHAR(50) NOT NULL,
ServiceId VARCHAR(50) NOT NULL,
ResourceId VARCHAR(50) NOT NULL, -- ✅ NEW: Resource per service
CustomPrice DECIMAL(10,2) NULL,
SortOrder INT NULL,
FOREIGN KEY (BookingId) REFERENCES Booking(Id),
FOREIGN KEY (ServiceId) REFERENCES Service(Id),
FOREIGN KEY (ResourceId) REFERENCES Resource(Id) -- ✅ NEW FK
);
CREATE TABLE CalendarEvent (
Id VARCHAR(50) PRIMARY KEY,
Start DATETIME NOT NULL,
End DATETIME NOT NULL,
Type VARCHAR(50) NOT NULL, -- ✅ Distinguishes customer vs vacation
BookingId VARCHAR(50) NULL, -- ✅ Nullable (only customer has booking)
ResourceId VARCHAR(50) NULL, -- ✅ Denormalized for performance
CustomerId VARCHAR(50) NULL, -- ✅ Denormalized for performance
FOREIGN KEY (BookingId) REFERENCES Booking(Id)
);
Code Changes Summary
Files Created (3)
- src/types/ResourceTypes.ts - IResource interface
- src/types/BookingTypes.ts - IBooking + IBookingService + CalendarEventType
- src/types/CustomerTypes.ts - ICustomer interface
Files Modified (2)
- src/types/ColumnDataSource.ts - Changed
Date | anytoDate | IResource - src/types/CalendarTypes.ts - Updated ICalendarEvent to use CalendarEventType
Documentation Created (1)
- docs/booking-event-architecture.md - 930 lines comprehensive guide
- 6 detailed examples
- Database schemas (SQL + IndexedDB)
- 5 business rules
- 4 common pitfalls
- Analytics patterns
- Query examples
Statistics
| Metric | Count |
|---|---|
| Time Spent | 2+ hours |
| Initial Estimate | 10 minutes |
| Major Iterations | 4 |
| Architectural Pivots | 3 |
| Developer Catches | 5+ critical issues |
| Interfaces Created | 5 (IResource, IBooking, IBookingService, ICustomer, CalendarEventType) |
| Interfaces Modified | 2 (IColumnInfo, ICalendarEvent) |
| Documentation Written | 930 lines |
| Examples Documented | 6 scenarios |
| Business Rules Defined | 5 |
| Common Pitfalls Identified | 4 |
| Database Tables Changed | 3 (Booking, BookingService, CalendarEvent) |
Developer Interventions (Critical Thinking Points)
1. "What if multiple resources work on same booking?"
Prevented: Single-resource assumption that would block split bookings
2. "Why should there be a primary resource?"
Prevented: Artificial hierarchy that doesn't match business reality
3. "Is vacation a booking?"
Resulted in: Clean separation - Booking = customers only, Events = everything
4. "If the student is sick?"
Resulted in: Service-level resource assignment enabling reassignment
5. "BookingType is gone now, right?"
Resulted in: Consistent naming - CalendarEventType for events, not bookings
What We Almost Built (And Avoided)
❌ Wrong Architecture 1: Primary Resource Model
Would have required:
- Complex logic to determine "who is primary"
- Arbitrary decision making (is master or student primary?)
- Doesn't support equal splits (Karina + Nanna)
❌ Wrong Architecture 2: Booking.resourceId + Service.resourceId
Would have resulted in:
- Redundant data
- Confusion about which resourceId to use
- "Primary" concept that doesn't exist in business logic
❌ Wrong Architecture 3: Vacation as Booking
Would have created:
- Optional fields everywhere (services? customer? for vacation?)
- Complex validation logic
- Confusing domain model
Final Architecture Benefits
✅ Clean Separation of Concerns
- Booking = Customer + Services (WHAT + HOW MUCH)
- CalendarEvent = Time + Resource (WHEN + WHO)
✅ Supports All Business Scenarios
- Simple booking (one customer, one service, one resource)
- Split session (one booking, multiple days)
- Split resources (student + master)
- Equal splits (two masters, no primary)
- Queued bookings (no time slot yet)
- Vacation/break (no booking, just time blocking)
✅ Type-Safe
- Required fields are enforced:
customerId: string,services: IBookingService[] - No optional fields that "sometimes" matter
- CalendarEventType clearly distinguishes booking vs non-booking events
✅ Flexible Resource Assignment
- Assign different resources per service
- Reassign if resource unavailable (student sick)
- No "primary" to maintain
✅ Query-Optimized
- CalendarEvent denormalizes resourceId for fast queries
- No JOIN needed to find "Karina's events"
- IndexedDB-friendly (no JOIN support)
Lessons Learned
1. Question Assumptions Early
My assumption: "Bookings are resource-bound" Reality: Bookings are customer-bound, resources are service-bound
2. Use Real Business Scenarios
Abstract discussions led to wrong models. Concrete examples ("student + master split") revealed the truth.
3. Don't Force Hierarchies
"Primary resource" seemed logical but didn't match business reality. Equal splits exist.
4. Separation of Concerns Matters
Mixing booking (business) with scheduling (time) created optional fields everywhere.
5. Developer Domain Knowledge is Critical
I had TypeScript expertise, but developer had business domain knowledge. The combination caught 5+ critical flaws before they became code.
Documentation Deliverables
booking-event-architecture.md (930 lines)
Sections:
- Core Principles - Separation of concerns
- Entity Definitions - Booking vs CalendarEvent
- Relationship Model - 1:N (one booking → many events)
- 6 Detailed Examples:
- Split session (same resource, multiple days)
- Split resources (master + student)
- Equal split (two masters)
- Queued booking
- Vacation (no booking)
- Simple reminder
- Database Schema - SQL (normalized) + IndexedDB (denormalized)
- API Data Flow - Backend JOIN → Frontend denormalization
- Business Rules - 5 rules with code examples
- Analytics & Reporting - Service duration vs actual time
- TypeScript Interfaces - Complete reference
- Common Pitfalls - 4 mistakes to avoid
- Summary Table - Quick reference
- Decision Guide - "Where does this belong?"
Build Results
✅ All TypeScript compilation successful
- 57 files processed
- 0 errors
- 0 warnings
- Build time: ~1.2s
✅ Type safety improved
- Removed
Date | any(was unsafe) - Added
Date | IResource(type-safe union) - Added CalendarEventType (was
string)
Conclusion
Initial request: "Create IResource interface" Time estimated: 10 minutes Time actual: 2+ hours
Why the difference?
Because the developer asked the right questions at the right time:
- "What if multiple resources work together?"
- "Why assume a primary?"
- "Is vacation really a booking?"
- "What if we need to reassign?"
Each question revealed a flaw in my assumptions. We iterated 4 times, discarded 3 approaches, and arrived at an architecture that actually matches the business model.
This is exactly how good software development should work.
The 2 hours spent thinking prevented weeks of refactoring later. Critical thinking and domain knowledge prevented premature optimization and over-engineering.
Next Steps (Not Part of This Session)
Phase 2: Resource Calendar Implementation
- Create ResourceColumnDataSource (returns IColumnInfo with IResource data)
- Create ResourceHeaderRenderer (renders resource headers instead of dates)
- Implement resource-mode switching (date-mode ↔ resource-mode)
- Add repository methods for resource queries
Phase 3: Booking Management
- Implement booking creation flow
- Service-to-event mapping logic
- Split-resource UI (assign services to different resources)
- Resource reassignment (when student is sick)
Session End
Key Takeaway: Slow down to speed up. 2 hours of architecture discussion saved weeks of wrong implementation.