Calendar/coding-sessions/2025-11-14-booking-resource-architecture-redesign.md
Janus C. H. Knudsen a360fad265 Redesigns booking and resource architecture
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
2025-11-14 23:05:57 +01:00

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 BookingTypeCalendarEventType
  • Clarified: Only ICalendarEvent uses this type, not IBooking
  • 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:

  1. Split Session: 180min service split across 2 days (same resource)
  2. Split Resources: Student + Master (different resources)
  3. 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)

  1. src/types/ResourceTypes.ts - IResource interface
  2. src/types/BookingTypes.ts - IBooking + IBookingService + CalendarEventType
  3. src/types/CustomerTypes.ts - ICustomer interface

Files Modified (2)

  1. src/types/ColumnDataSource.ts - Changed Date | any to Date | IResource
  2. src/types/CalendarTypes.ts - Updated ICalendarEvent to use CalendarEventType

Documentation Created (1)

  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

  1. Simple booking (one customer, one service, one resource)
  2. Split session (one booking, multiple days)
  3. Split resources (student + master)
  4. Equal splits (two masters, no primary)
  5. Queued bookings (no time slot yet)
  6. 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:

  1. Core Principles - Separation of concerns
  2. Entity Definitions - Booking vs CalendarEvent
  3. Relationship Model - 1:N (one booking → many events)
  4. 6 Detailed Examples:
    • Split session (same resource, multiple days)
    • Split resources (master + student)
    • Equal split (two masters)
    • Queued booking
    • Vacation (no booking)
    • Simple reminder
  5. Database Schema - SQL (normalized) + IndexedDB (denormalized)
  6. API Data Flow - Backend JOIN → Frontend denormalization
  7. Business Rules - 5 rules with code examples
  8. Analytics & Reporting - Service duration vs actual time
  9. TypeScript Interfaces - Complete reference
  10. Common Pitfalls - 4 mistakes to avoid
  11. Summary Table - Quick reference
  12. 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:

  1. "What if multiple resources work together?"
  2. "Why assume a primary?"
  3. "Is vacation really a booking?"
  4. "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.