Skip to content

🏨 Hotel Booking System — Interview Questions

Q1: Two guests click "book" on the last suite at the same moment. Walk me through what happens.

Answer: - Both requests may have seen the suite in searchAvailable; that result is only a hint. - Both call hold, which calls RoomCalendar.tryReserve on the suite. The check (isFree) and the insert happen under the room's monitor, so the second thread sees the first reservation and gets false. It tries the next room of that type, finds none, and throws NoAvailabilityException. - In a database the same invariant is enforced by the storage layer, not by application code: a unique key on (room_id, night) (one row per night), or a Postgres exclusion constraint EXCLUDE USING gist (room_id WITH =, stay WITH &&) on a daterange column. Then two transactions cannot both commit.

Q2: How do you check whether a new stay conflicts with existing ones efficiently?

Answer: - Keep each room's reservations in a TreeMap<LocalDate, Reservation> keyed by check-in. They never overlap each other. - For a new [in, out): look at floorEntry(in) (does it end after in?) and higherEntry(in) (does it start before out?). Nothing else can overlap. O(log n). - Alternative: an inventory counter per (room_type, night). Checking a 3-night stay touches 3 rows; it answers "how many are left" cheaply but does not assign a room. Many real systems sell by type with counters and assign room numbers at check-in.

Q3: Payment takes 30 seconds and sometimes fails. How do you avoid holding rooms forever or selling them twice?

Answer: - Two-phase: hold reserves rooms and locks the price with a TTL (15 minutes here); confirm after payment succeeds. - An expiry sweeper (expireHolds) moves stale holds to EXPIRED and releases rooms. confirm on an expired hold fails, and the payment must be voided or refunded. - Race between a late confirm and the sweeper: both use a compare-and-set on the status (transition(HELD, ...)). Exactly one wins, and only the winner releases rooms. - In production the hold is a DB row with expires_at; the sweeper is a periodic job with UPDATE ... WHERE status='HELD' AND expires_at < now(), which is atomic per row.

Figure: two-phase booking with a TTL hold.

sequenceDiagram
  participant G as Guest
  participant B as Booking service
  participant P as Payment
  G->>B: hold (idempotency key)
  B-->>G: Hold with price lock, 15 min TTL
  G->>P: Pay
  P-->>B: Success
  B->>B: transition(HELD to CONFIRMED)
  Note over B: Sweeper transition(HELD to EXPIRED) races, one wins

Q4: The client times out and retries the booking request. What stops a duplicate booking?

Answer: - An idempotency key generated by the client per checkout attempt. hold stores key → booking; a retry returns the same booking. - Reusing a key with a different payload is a client bug, so reject it rather than silently returning a mismatched booking. - In a service: a table idempotency(key PK, request_hash, booking_id) written in the same transaction as the reservation.

Q5: Now add group bookings: 10 rooms, all or nothing.

Answer: - hold(..., roomCount = 10) reserves rooms one by one via tryReserve; if it gets fewer than 10 it releases the ones it took and fails. - Trade-off: under heavy contention two group requests can each grab some rooms, both fail and both roll back (wasted work, never incorrect). Alternatives: lock the candidate rooms in a fixed order (id order) and check all before inserting, or do it in one DB transaction. - Real hotels also do allotments: block N rooms for a group until a cut-off date, then release unclaimed rooms to general inventory.

Q6: Now let guests change their dates.

Answer: - Reserve the new range first (new hold), then cancel the old booking. Never release first: if the new range is unavailable the guest has lost their original room. - If the new range overlaps the old one on the same room, the naive approach conflicts with itself. Either allow the calendar to ignore the guest's own booking id when checking, or extend/shrink in place under the room lock.

Q7: How do you handle overbooking?

Answer: - Hotels overbook deliberately because of no-shows (typically a few percent). Model it as a per-type cap (capacity × (1 + overbookRate)) on sellable inventory, separate from physical rooms. - Room numbers are then assigned at check-in; if the hotel is physically full, the guest is "walked" to a partner hotel at the hotel's cost. Bump the lowest-value reservation first (non-loyalty, lowest rate). - This only works with type-level counters, which is why many systems sell by type and assign rooms late.

Q8: How would you implement a waitlist?

Answer: - On CANCELLED or EXPIRED, emit an event (roomType, range). - A waitlist consumer finds the oldest waiting request whose range is now free and calls hold on their behalf, then notifies them with a time-limited payment link (the hold's TTL). - Priority (loyalty tier first, then FIFO) is a sort key on the waitlist. Never hold the room "for" someone without a TTL.

Q9: How do you test this?

Answer: - Pure unit tests for DateRange.overlaps (back-to-back, nested, partial) and for pricing with exact BigDecimal expectations. - Lifecycle tests with an injected mutable Clock: confirm after TTL fails, sweeper is idempotent, refund at 8 days vs. 30 hours before check-in. - Concurrency: N threads racing for K rooms on the same dates must produce exactly K successes; then a random hold/cancel storm followed by an invariant check that no room's reservations overlap. Assert the invariant, not a specific interleaving.


⚠️ Common mistakes

  • Closed-interval overlap, so checkout day and next check-in conflict.
  • Availability as "rooms of this type" without looking at dates, then assigning the first room of the type to everyone (actual double booking).
  • Check availability, then reserve, as two separately locked steps.
  • Releasing inventory on checkout, or allowing cancel to run twice and release twice.
  • double for money; pricing that reads LocalDate.now() directly.
  • A single synchronized on the whole service. It is correct but serialises every booking in the hotel; per-room locks are just as simple.
  • No hold expiry, so abandoned checkouts leak inventory.

🎯 Senior vs Staff signal

  • Senior: half-open ranges, per-room conflict check that is better than a full scan, atomic check-and-insert, a hold → confirm lifecycle with expiry, exact money, deterministic tests.
  • Staff: puts the invariant where it cannot be bypassed (DB constraint, not just application locks), explains type-level counters vs. room-level assignment and when each is right (overbooking, late room assignment), handles retries with idempotency keys, reasons about the confirm/expire race and payment compensation, and knows how inventory sync with OTAs (channel manager) creates the hardest double-booking cases in practice.