Vasu Patel logoVasu Patel
← Whiteboard to Blueprint: HLD & LLD
All chapters in this track
  1. 01Chapter 1: What HLD and LLD Are, and How They Differ
  2. 02Chapter 2: Gathering and Clarifying Requirements Before Designing Anything
  3. 03Chapter 3: HLD Deliverables: Architecture, Components, Technology Choices, Data Flow
  4. 04Chapter 4: LLD Deliverables: Class Diagrams and Sequence Diagrams
  5. 05Chapter 5: LLD Deliverables: Database Schema and API Contracts
  6. 06Chapter 6: Documenting Decisions: ADRs and Communicating Trade-offs to Stakeholders
  7. 07Chapter 7: Moving Cleanly from HLD to LLD Without Losing Intent
  8. 08Chapter 8: Common Mistakes: Over-Designing, Under-Specifying, and Skipping Non-Functional Requirements
  9. 09Chapter 9: When LLD Contradicts Its Own HLD
  10. 10Chapter 10: HLD and LLD in Design Reviews: What Reviewers Actually Look For
  11. 11Chapter 11: Worked Example: Ride-Sharing Dispatch — Requirements Through HLD to LLD
  12. 12Chapter 12: Worked Example: Hotel Booking — Requirements Through HLD to LLD
  13. 13Chapter 13: Worked Example: Food Delivery — Requirements Through HLD to LLD
  14. 14Chapter 14: How HLD and LLD Show Up in Interviews and Design Reviews
  15. 15Chapter 15: Common Interview Questions About HLD and LLD

Chapter 12 of 15

Chapter 12: Worked Example: Hotel Booking — Requirements Through HLD to LLD

Why this matters

The hotel booking domain has appeared in fragments throughout this book's earlier chapters — this chapter assembles those fragments into one complete, coherent worked example, and adds the piece those fragments hadn't yet addressed: the double-booking prevention problem, which is this system's genuinely hardest design challenge.

Requirements (Chapter 2)

Clarifying questions: "Can a booking be modified/cancelled after confirmation, and what are the rules (refund policy, cutoff time)? Do we need to support overbooking intentionally (a common hotel industry practice, betting on some cancellations)? What's the expected scale?"

Assumed answers: bookings can be cancelled up to 24 hours before check-in with a full refund; intentional overbooking is NOT in scope for this design (a simplifying assumption, stated explicitly); assume 10,000 hotels, averaging 100 rooms each, with booking volume of roughly 500,000 bookings/month system-wide.

HLD

Architecture diagram:

+----------+     +----------------+     +-------------------+
|  Client  |---->| API Gateway    |---->| Booking Service     |
+----------+     +----------------+     +-------------------+
                                                    |
                                        +-----------+-----------+
                                        v                        v
                              +-------------------+   +-------------------+
                              | Booking Database   |   | Payment Service    |
                              | (source of truth)   |   | (external/internal)|
                              +-------------------+   +-------------------+
                                        |
                                        v
                              +-------------------+
                              | Notification       |
                              | Service (async)     |
                              +-------------------+

Component responsibilities: Booking Service owns the booking lifecycle and — critically — availability validation; Payment Service owns payment authorization and capture, kept separate specifically because payment processing has distinct compliance/security requirements (PCI-DSS) that benefit from isolation (companion System Design book, Chapter 11's genuine-technical-need justification for a service boundary).

Technology choice, with reasoning: "PostgreSQL for the booking database, specifically because preventing double-bookings requires genuine transactional guarantees (companion Database book, Chapter 3) — this is a case where a relational database's strong consistency is a hard requirement, not a default choice made out of habit."

Data flow — "create a booking":

1. Client requests a booking for a specific room and date range.
2. Booking Service checks availability WITHIN A TRANSACTION.
3. Booking Service creates the booking record (still within the same transaction).
4. Booking Service calls Payment Service to authorize payment.
5. On successful authorization, the transaction commits.
6. Booking Service publishes a "booking.created" event (asynchronous, off
   the critical path -- companion System Design book, Chapter 8).
7. Notification Service sends confirmation, AFTER the client has already
   received their response in step 5.

Capacity estimate: 500,000 bookings/month ≈ 0.19 bookings/second average — a genuinely low-scale system by the companion System Design book's Chapter 4 standards; explicitly, sharding is not remotely justified here, and a single, well-provisioned PostgreSQL instance with standard replication for availability is more than sufficient.

LLD — deep-dive on double-booking prevention (the genuinely hardest part)

Why this is the hardest part: "The core design risk here is two concurrent booking requests for overlapping dates on the same room both passing an availability check before either commits — exactly the check-then-act race condition from the companion Database book's Chapter 3, now appearing in this specific domain."

Sequence diagram, showing the correct, race-free flow:

Client A        BookingService           Database
   |                  |                      |
   |--createBooking-->|                      |
   |                  |--BEGIN TRANSACTION-->|
   |                  |--SELECT ... FOR UPDATE (lock rows for this room/date range)
   |                  |<--rows locked--------|
   |                  |  (Client B's request, if concurrent, BLOCKS here
   |                  |   until Client A's transaction completes)
   |                  |--INSERT booking----->|
   |                  |--COMMIT------------->|
   |<--confirmed------|                      |

Database schema, with the specific constraint that structurally prevents overlap:

CREATE TABLE bookings (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    room_id UUID NOT NULL REFERENCES rooms(id),
    check_in DATE NOT NULL,
    check_out DATE NOT NULL CHECK (check_out > check_in),
    status VARCHAR(20) NOT NULL DEFAULT 'confirmed',
    -- EXCLUSION CONSTRAINT: PostgreSQL-specific, structurally prevents
    -- ANY two rows for the same room from having overlapping date ranges,
    -- enforced by the database itself regardless of application logic:
    EXCLUDE USING gist (
        room_id WITH =,
        daterange(check_in, check_out) WITH &&
    ) WHERE (status = 'confirmed')
);

Why the exclusion constraint matters beyond the transaction/locking approach: "The SELECT ... FOR UPDATE transaction handles the concurrency race correctly, but the exclusion constraint is a second, independent, structural safety net — even if application code somewhere has a bug that bypasses the intended locking logic, the database itself will reject an overlapping booking outright, exactly the OOP book's Chapter 1 and Database book's Chapter 2 'structural prevention over convention' principle, applied with real belt-and-suspenders redundancy here specifically because overbooking has a real, costly, reputation-damaging business impact."

Common mistakes

  1. Relying only on application-level locking without a database-level structural constraint as a second safety net, leaving the system vulnerable to a future application bug that bypasses the intended locking logic entirely.
  2. Not stating the overbooking-is-out-of-scope assumption explicitly, risking a stakeholder later assuming the system supports a common industry practice that was actually never designed for.
  3. Choosing a NoSQL document store for booking data "for flexibility" without recognizing the hard, non-negotiable transactional requirement this specific domain has for preventing double-bookings.
  4. Not producing a sequence diagram for the booking-creation flow, missing the chance to make the transaction/locking behavior explicit and reviewable before implementation.

Chapter summary / cheat sheet

  • Hotel booking's genuinely hard problem: preventing double-bookings under concurrent requests — solved with both a transactional locking approach (SELECT ... FOR UPDATE) AND a structural database exclusion constraint as an independent, redundant safety net.
  • The relational database choice here is justified by a hard, non-negotiable transactional requirement, not habit — a case where the companion Database book's Chapter 1 relational-vs-NoSQL decision framework clearly favors relational.
  • At this system's actual estimated scale (well under 1 booking/second average), sharding is explicitly not justified — a good example of Chapter 8's "avoid over-designing" discipline in action.
  • This worked example demonstrates layering a structural database constraint on top of application-level concurrency control as genuine defense in depth for a business-critical invariant.