Hotel Booking Interview Questions | JiQuest

add

#

Hotel Booking

System design deep dive · HLD

Design a Hotel Booking System: full high-level design.

Requirements, back-of-envelope capacity estimation, a high-level design diagram, a deeper deployment architecture diagram, sequence diagrams for date-range availability search and for reservation with overbooking prevention, and an entity-relationship diagram for hotels, rooms, reservations, and rate plans - with the reasoning an interviewer expects behind every box and arrow.

2MHotels in inventory
500:1Search : booking ratio
0Tolerated double-bookings
Guestsearches dates Search serviceavailability + price Availability cacheper room-night Reservation svcatomic hold + book Inventory DBsource of truth confirms booking

1. Clarify requirements before drawing any box

Hotel booking looks like a CRUD app until you notice the core invariant: two guests must never both be confirmed for the same physical room on the same night. Everything about inventory, caching, and consistency is downstream of protecting that one guarantee.

Functional requirements

Search availabilityGiven a location, date range, and guest count, return hotels/room types with open inventory and price.
Reserve a roomGuest confirms a room type for a date range; the reservation must never exceed physical room capacity.
Rate-plan pricingPrice varies by date, length of stay, cancellation policy, and promotional rate plan.
Modify / cancelGuests can change dates or cancel, releasing inventory back for others to book, subject to the rate plan's policy.

Non-functional requirements

No overbookingRoom-night inventory must never go negative; this is a hard correctness constraint, not a best-effort one.
Fast searchAvailability search across a date range and thousands of hotels must return in well under a second.
Search-heavy scaleSearches vastly outnumber actual bookings; the search path must not contend with the booking path's locks.
Regional consistency, global searchA booking for one hotel only needs strong consistency at that hotel; search can tolerate a few seconds of staleness globally.
Explicitly out of scope Payment processing/settlement, hotelier-facing channel-manager integrations with third-party OTAs, and loyalty-points accrual are called out as extensions rather than core requirements, so the core design stays focused on search, availability, and overbooking-safe reservation.

2. Back-of-the-envelope capacity estimation

These numbers decide whether availability can be served from a cache (yes, aggressively) and how the reservation path's write contention is bounded to a single room type at a single hotel rather than the whole system.

MetricAssumptionResulting estimate
Inventory size2 million hotels, ~80 rooms average, ~3 room types/hotel~160 million physical rooms; availability tracked per (room_type, date) not per physical room
Search requests~50 million searches/day globally~580 searches/sec average, ~5,000/sec peak (weekend/holiday planning surges)
Bookings~100,000 confirmed bookings/day~1.2 bookings/sec average, ~50/sec peak - a search:booking ratio of roughly 500:1
Availability cache sizeRoom types × a 365-day rolling search window2M hotels × 3 room types × 365 days ≈ 2.2 billion (room_type, date) cells - fits a sharded in-memory/columnar cache, refreshed on booking events
Reservation write contentionConcurrent booking attempts for the same room_type+date rangeBounded to a single hotel's single room type per night - trivially shardable, never a global lock
Why this matters A 500:1 search-to-booking ratio is the number that justifies serving nearly all availability search results from an aggressively cached, slightly-stale read path, while reserving the strongly consistent, lock-guarded write path exclusively for the rare moment a guest actually commits to booking a specific room type and date range.

3. High-level design (HLD)

The HLD separates the read-heavy, cache-friendly search path from the low-volume, correctness-critical reservation path - both ultimately resolve against the same inventory database, but only the reservation path is allowed to write to it.

Guest (search)city, dates, guests Search servicereads cache only Availability cachedenormalized, per date Pricing/rate-plan svcrate-plan resolution Guest (book)selects a room Reservation servicehold → confirm Inventory DBsource of truth, sharded Notification/CDCrefreshes cache
Stateless servicesCache / fast-path infraDurable source of truthAsync / supporting

What each box owns

Search service & availability cache

Resolves a location/date-range/guest-count query entirely against a denormalized, per-(room_type, date) availability cache - it never touches the inventory database directly. This cache can lag reality by a few seconds without any real harm, since the reservation path re-validates availability authoritatively at booking time regardless of what search showed.

Reservation service & inventory DB

Owns the only write path to room-night inventory. A booking is a two-step "hold, then confirm" transaction: first decrement available count for every date in the stay (atomically, guarded so it can never go below zero), then finalize the reservation record. This is the one part of the system where correctness strictly outranks latency or throughput.

Pricing / rate-plan service

Resolves the final nightly price for a given room type, date range, and selected rate plan (flexible vs non-refundable, length-of-stay discounts, promotional codes). Pricing is deliberately decoupled from availability - a room can be available at multiple different prices depending on which rate plan the guest qualifies for or selects.

CDC / notification pipeline

Every confirmed booking or cancellation in the inventory DB emits a change event (via change-data-capture or an explicit outbox) that asynchronously updates the availability cache. This keeps the cache "close enough" to real time for search purposes without the reservation path ever waiting on a cache write to complete.

4. Detailed architecture diagram

The architecture diagram shows inventory sharded by hotel_id (so no booking anywhere in the world contends with another hotel's booking), a globally distributed availability cache, and rate-plan pricing computed independently of the write-locked inventory path.

Edge layer CDN (static content) API gateway + L7 LB Search query planner Idempotency layerdedupes retried bookings Search tier (globally cached, eventually consistent) Search svc ×40 pods Availability cachesharded by geographyrefreshed via CDC, ~seconds lag Reservation tier (strongly consistent) Reservation svc ×24 pods Inventory DB, sharded by hotel_idrow-level lock per room_type+date1 primary + 2 replicas/shard Rate-plan/pricing svc CDC / cache-refresh pipeline Change-data-capture stream Cache-updater workers every confirmed booking or cancellation decrements/increments the cache Hoteliers' channel/PMS integration (async) Property management system sync, rate updates writes go through the same inventory DB, same guarantees
DecisionChoiceReasoning
Inventory sharding keyHash of hotel_idEvery booking is scoped to a single hotel's room inventory; sharding on hotel_id guarantees write contention never crosses hotels, however busy the platform gets globally.
Search consistencyEventually consistent cache, refreshed via CDCSearch results being a few seconds stale is an acceptable trade-off given the reservation path always re-checks authoritative inventory before confirming.
Overbooking preventionAtomic conditional decrement at the inventory DB, not application-level checksA read-then-write check in application code is racy under concurrency; the decrement itself must be the atomic operation that both checks and reserves.
IdempotencyClient-supplied idempotency key on every booking requestA guest's retried request (double-tap, network timeout) must not create two reservations or double-decrement inventory for the same intended booking.

5. Sequence diagrams for the two critical flows

Search shows how the cache-only read path stays fast and decoupled; reservation shows exactly where the atomic conditional decrement prevents two guests from ever winning the same room-night.

5.1 Room availability search across a date range

Guest Search svc Availability cache Pricing svc 1. search(city, check-in, check-out, guests) 2. query cells for every date in range, per hotel 3. candidate room_types with available_count > 0 for all nights 4. resolve nightly price per candidate 5. total price per stay 6. ranked results with prices

Step 3's "available for all nights" check requires the room_type to have a positive count on every single date in the requested range, not just the check-in date - a common bug is checking only the first night. Every one of these steps reads from a cache or a stateless pricing computation; none of them touch the inventory database, which is exactly why search can absorb 5,000 requests/sec without any risk of lock contention with actual bookings.

5.2 Reservation with overbooking prevention

Guest Reservation svc Inventory DB Availability cache 1. book(room_type, dates, idempotency_key) 2. UPDATE ... SET count = count - 1 WHERE count > 0 (per date, txn) 3a. all dates succeeded (rows affected = nights) 3b. any date had count=0 → rollback entire txn 4. INSERT reservation (confirmed) 5. publish inventory-changed event (async) 6. 201 Confirmed, confirmation number

Step 2 is the entire mechanism: the conditional WHERE count > 0 makes the check and the decrement one atomic database operation instead of two separate steps a race condition could slip between - this is what makes overbooking structurally impossible rather than merely unlikely. Step 3b shows the multi-night case: if the stay spans 3 nights and any single night is already fully booked, the entire transaction rolls back rather than partially reserving some nights, since a "half-booked" stay is not a valid reservation. Step 5's cache update is deliberately asynchronous and after the fact - the guest's confirmation in step 6 never waits on it.

6. Entity-relationship (ER) diagram and schema

The schema has to answer: how is per-night inventory tracked without one row per physical room, how does a reservation span multiple nights atomically, and how do rate plans attach pricing rules without duplicating them per date.

hotels PK id BIGINT name, city VARCHAR star_rating INT geo POINT rooms (room_type_inventory) PK id BIGINT FK hotel_id BIGINT room_type VARCHAR total_rooms INT date DATE available_count INT UQ (hotel_id, room_type, date) reservations PK id BIGINT FK hotel_id BIGINT FK room_type_id BIGINT FK rate_plan_id BIGINT check_in/out DATE status ENUM UQ idempotency_key VARCHAR rate_plans PK id BIGINT FK room_type_id, nightly_rate cancellation_policy ENUM 1N 1N 1N 1N one hotel has many room_type+date inventory rows; one room type has many rate plans; a reservation references one room_type and one rate_plan and holds a unique idempotency_key

Key modeling decisions

rooms is per (hotel, room_type, date), not per physical roomGuests book a room type, not a specific numbered room; tracking a count per date avoids needing to assign and track individual room identities until check-in.
reservations.idempotency_key is uniquely constrainedA retried booking request with the same key is rejected as a duplicate at the database level, not just detected at the application layer - the strongest possible guarantee against double-booking from client retries.
rate_plans is decoupled from the date-level inventory rowMultiple rate plans (flexible, non-refundable, member rate) can price the same room_type+date differently without needing separate inventory rows per plan.
available_count is decremented per date, per reservation spanA 3-night stay touches 3 separate (room_type, date) rows in one transaction, which is why the reservation transaction must be all-or-nothing across every date in the stay.
Storage choiceUse whenWatch out for
Relational (Postgres/MySQL), sharded by hotel_id, for rooms/reservationsOverbooking prevention requires atomic, transactional conditional writes across multiple date rows at once.A very long stay (many nights) means locking many rows in one transaction; length-of-stay caps or careful lock ordering avoid excessive contention.
Wide denormalized cache (Redis/columnar) for the availability read pathAccess pattern is "is this room type open for this whole date range," at very high read volume, tolerant of staleness.Never treated as authoritative - the reservation path always re-validates against the relational inventory DB before confirming.

7. Deep dives interviewers actually probe

How exactly does the atomic decrement prevent a race condition that a naive check-then-write wouldn't?

A naive implementation reads available_count, checks it's > 0 in application code, then issues a separate UPDATE to decrement it. Between the read and the write, another request can read the same (now-stale) count and also decide it's safe to book - both succeed, and the room is oversold. Folding the check into the WHERE clause of the UPDATE itself (SET count = count - 1 WHERE count > 0) makes the database's own row-level locking do the serialization: only one of two concurrent transactions can win the conditional update, and the loser's statement affects zero rows, which the application interprets as "sold out, try another room type."

What happens if a guest's booking request times out on their end but actually succeeded server-side?

The client (correctly) doesn't know if its request succeeded, and retries with the same idempotency_key. Because that key has a unique constraint on the reservations table, the retried request's INSERT fails harmlessly (or the service short-circuits and returns the original confirmation) instead of creating a second reservation and double-decrementing inventory. This is why idempotency keys are treated as a core correctness mechanism, not just a nice-to-have API convention.

How is a hold/cart-style temporary reservation handled during checkout?

Many booking flows briefly "hold" inventory (e.g. for 10-15 minutes) while a guest completes payment, so it isn't sold to someone else mid-checkout. This is modeled as a reservation in a pending state with a short expiry - the same atomic decrement reserves the inventory immediately, but a background job releases (increments back) any pending reservation whose hold window expires without payment completing, converting a pending reservation to expired rather than confirmed.

How does search stay fast when a query spans a wide date range across thousands of hotels?

The availability cache is denormalized specifically to answer "is this room type open for every night in this range" as a single range query per hotel/room_type, rather than joining across per-night rows at query time. Geographic sharding of the cache (by city/region) also means a search for "hotels in Tokyo" never scans data for hotels anywhere else, keeping each shard's working set small and cache-friendly.

What is the single biggest bottleneck as this scales 10x?

Not search - the cache tier scales horizontally by geography. The real bottleneck becomes write contention on a small number of extremely popular hotels during a demand spike (a major event driving thousands of simultaneous booking attempts at the same few properties) where many transactions compete for locks on the same room_type+date rows. The fix is finer-grained row locking (per date rather than per room_type spanning all dates) combined with a request queue that serializes contested bookings for that specific hotel rather than letting them all hit the database simultaneously and mostly fail.

8. Summary: what a strong answer covers

Separated cache-only search from write-locked bookingJustified every number with a calculationMade the conditional decrement the correctness mechanism Sharded inventory by hotel_id, never a global lockUsed idempotency keys to prevent duplicate bookingsDecoupled rate-plan pricing from availability
Interview tip When asked to design a hotel booking system, the strongest signal is naming overbooking prevention as the one non-negotiable correctness constraint and then showing, concretely, which single atomic database operation enforces it - everything else in the design (caching, sharding, idempotency) exists to keep that one operation fast and uncontended, not to replace it.
No comments
Leave a Comment