Skip to content

🧠 Hotel Booking System — Thought Process

📊 Class Diagram

classDiagram
    class HotelBookingService {
        -Map~RoomType, List~RoomCalendar~~ calendarsByType
        -Map~String, RoomCalendar~ calendarsById
        -PricingStrategy pricing
        -Clock clock
        -Duration holdTtl
        -Map~String, Booking~ bookings
        -Map~String, Booking~ byIdempotencyKey
        +searchAvailable(type RoomType, stay DateRange) List~Room~
        +hold(idempotencyKey String, guest Guest, type RoomType, stay DateRange, roomCount int, policy CancellationPolicy) Booking
        +confirm(bookingId String) Booking
        +cancel(bookingId String) BigDecimal
        +checkIn(bookingId String) void
        +checkOut(bookingId String) void
        +expireHolds() int
        +get(bookingId String) Booking
    }
    class RoomCalendar {
        -Room room
        -TreeMap~LocalDate, Reservation~ byCheckIn
        +isFree(r DateRange) boolean
        +tryReserve(bookingId String, r DateRange) boolean
        +release(bookingId String, r DateRange) void
        +reservations() List~Reservation~
    }
    class Reservation {
        <<record>>
        +String bookingId
        +DateRange range
    }
    class Room {
        <<record>>
        +String id
        +RoomType type
        +int floor
    }
    class DateRange {
        <<record>>
        +LocalDate checkIn
        +LocalDate checkOut
        +nights() long
        +overlaps(o DateRange) boolean
    }
    class Guest {
        <<record>>
        +String id
        +String name
        +LoyaltyTier tier
    }
    class Booking {
        -String id
        -Guest guest
        -RoomType roomType
        -List~String~ roomIds
        -DateRange stay
        -BigDecimal total
        -BigDecimal firstNight
        -CancellationPolicy policy
        -Instant holdExpiresAt
        -BookingStatus status
        +transition(expected BookingStatus, next BookingStatus) boolean
        +moveTo(next BookingStatus) BookingStatus
        +sameRequest(g Guest, t RoomType, s DateRange, count int) boolean
    }
    class BookingStatus {
        <<enumeration>>
        HELD
        CONFIRMED
        CHECKED_IN
        CHECKED_OUT
        CANCELLED
        EXPIRED
        +canMoveTo(next BookingStatus) boolean
    }
    class PricingStrategy {
        <<interface>>
        +price(type RoomType, stay DateRange, guest Guest) BigDecimal
    }
    class NightlyRatePricing {
        -BigDecimal weekendSurcharge
        +price(type RoomType, stay DateRange, guest Guest) BigDecimal
    }
    class LoyaltyDiscountPricing {
        -PricingStrategy inner
        +price(type RoomType, stay DateRange, guest Guest) BigDecimal
    }
    class CancellationPolicy {
        <<enumeration>>
        FLEXIBLE
        MODERATE
        NON_REFUNDABLE
        +refund(total BigDecimal, firstNight BigDecimal, beforeCheckIn Duration) BigDecimal
    }
    class RoomType {
        <<enumeration>>
        SINGLE
        DOUBLE
        SUITE
        +BigDecimal nightlyRate
    }
    class LoyaltyTier {
        <<enumeration>>
        NONE
        SILVER
        GOLD
        PLATINUM
        +BigDecimal discount
    }

    HotelBookingService "1" *-- "*" RoomCalendar : one per room
    HotelBookingService "1" *-- "*" Booking : bookings
    HotelBookingService o-- PricingStrategy : pricing
    RoomCalendar --> Room
    RoomCalendar "1" *-- "*" Reservation : non-overlapping
    Reservation --> DateRange
    Room --> RoomType
    Guest --> LoyaltyTier
    Booking --> Guest
    Booking --> DateRange
    Booking --> BookingStatus
    Booking --> CancellationPolicy
    Booking --> RoomType
    NightlyRatePricing ..|> PricingStrategy
    LoyaltyDiscountPricing ..|> PricingStrategy
    LoyaltyDiscountPricing o-- PricingStrategy : wraps (decorator)

Problem Breakdown

Step 1: Core Entities

  • Room: id, type, floor. The unit that can actually be double-booked.
  • Guest: id, name, loyalty tier.
  • DateRange: half-open [checkIn, checkOut).
  • Booking: guest, assigned room(s), stay, locked price, cancellation policy, status.

Step 2: Date Range Model

  • Half-open ranges, so the checkout day is free for the next guest. Get this wrong and back-to-back stays conflict.
  • Overlap test: a.in < b.out && b.in < a.out.
  • Per room, keep reservations in a TreeMap keyed by check-in; only the neighbours of a new check-in can overlap it, so the check is O(log n).

Step 3: Where does double booking get prevented?

  • At the moment of reservation, not at search time. Search is advisory and can be stale.
  • Check-and-insert must be one atomic step per room (RoomCalendar.tryReserve). In a database the same thing is a unique key on (room_id, night) or a Postgres exclusion constraint.

Step 4: Booking Lifecycle

  • HELD (rooms reserved, price locked, TTL) → CONFIRMED after payment → CHECKED_IN → CHECKED_OUT; CANCELLED and EXPIRED are terminal.
  • Holds exist because payment is slow and can fail; without a TTL, abandoned checkouts leak inventory.

Step 5: Pricing and Refunds

  • Strategy for the base rule, Decorator to stack discounts. BigDecimal, never double.
  • Price computed once at hold time and stored.
  • Refund depends on policy and time before check-in, so inject a Clock.

Step 6: Concurrency

  • Per-room locks, no global lock.
  • Status changes are compare-and-set; only the winner releases rooms, so inventory is never released twice.
  • Idempotency key on hold so client retries are safe.

Key Decisions

Decision Why
Assign a specific room at hold time Double booking is a property of a room, not a type; you can't prove its absence with type-level counts alone in an LLD
Half-open DateRange Checkout day is sellable
TreeMap per room O(log n) conflict check using floor/higher neighbours
Hold with TTL, then confirm Payment is slow and fallible; inventory must come back if it fails
Price locked at hold Guest pays what they were quoted
BigDecimal + injected Clock Exact money, deterministic tests

⏱️ How to run this in a 45–60 min interview

Time Step What to say out loud
0–7 min Clarify "One hotel or many? Book a room type or a specific room? Do we need payment, holds, cancellation refunds, group bookings? I'll assume one hotel, the guest picks a type, we assign a room, and payment happens between hold and confirm."
7–15 min Entities and interfaces DateRange (state half-open explicitly), Room, Guest, Booking, BookingStatus, PricingStrategy, and the service API: searchAvailable, hold, confirm, cancel, checkIn, checkOut.
15–35 min Core code overlaps, RoomCalendar.isFree with floor/higher entries, tryReserve, hold walking candidate rooms. Walk an example: [5,8) then [8,10) on the same room succeeds, [7,9) fails.
35–45 min Concurrency "Search is a hint; correctness is check-and-insert under the room's lock. Two people racing for the last room: one tryReserve wins. Status changes are CAS so a late confirm and the expiry sweeper can't both release the room."
45–60 min Extension Group booking (all-or-nothing with rollback), idempotent retries, modify dates (new hold before releasing the old), overbooking, or moving the invariant into the database.

Clarifying questions worth asking

  • Does the guest book a room type or a specific room? When is the room number assigned?
  • Is payment in scope? Do we hold inventory during checkout, and for how long?
  • Cancellation rules: free until when, and what is charged after?
  • Multi-room / group bookings: all-or-nothing?
  • One property or a chain? (Changes the sharding answer in the HLD part.)
  • Is overbooking allowed?

What to avoid

  • Closed-interval overlap (<=), so back-to-back stays conflict.
  • Checking availability in one call and reserving in another without holding a lock (check-then-act).
  • Releasing inventory on checkout, or on every call to cancel.
  • double for money; LocalDate.now() sprinkled through the code.