Sign in
TodayMapLearnPracticeReview
Library
17 MINinterviewCase StudiesNot started

Design Hotel Reservation

Design a hotel booking system (Booking.com / Expedia). Covers inventory as a (hotel_id, room_type_id, date) cube, the concurrency problem of two users booking the last room (pessimistic vs optimistic locking), overbooking as a deliberate strategy, the search-and-filter query for available hotels by date range, and the reservation state machine with payment + cancellation flows. The deep dive walks through why naive `SELECT FOR UPDATE` on per-date rows is too coarse and how to model inventory at the date grain with version counters.

Why this matters

Hotel reservations are the canonical 'inventory with concurrency' problem. Every booking platform — airlines, movie tickets, restaurants, concert venues, hospital appointments — faces the same fundamental challenge: multiple users competing for a finite, time-bucketed resource, with cancellation, expiration, and overbooking on top. The patterns here (date-cube inventory, optimistic locking via version, idempotent holds, overbooking math) are reusable across any reservation domain. Booking.com handles millions of bookings per day with this exact architecture.

Prerequisites
  • Transactions & ACID
  • SQL vs NoSQL
  • Isolation Levels
  • Sharding
Related
  • Transactions & ACID
  • Isolation Levels
  • Design Rate Limiter
Used in

Foundational.

Lesson

How it works

What are we designing? A hotel reservation system. A user searches for available hotels by city + date range + guest count; sees results with prices; books a specific room type; the system holds the inventory, takes payment, and confirms. Other users searching concurrently must not see the same room as available. Cancellations return inventory to the pool.

The defining challenges are (1) inventory as a time-cube — a hotel has rooms of multiple types, and each room type has independent availability per night, so the inventory is a (hotel_id, room_type_id, date) cube, (2) concurrency control — when two users race for the last room, exactly one must win and the other must see 'unavailable', and (3) overbooking — airlines and hotels deliberately sell more seats than they have, betting on no-shows; we must support this safely.

Functional requirements.

  • Search hotels by city, check-in date, check-out date, guest count, with filters (stars, amenities, price).
  • View real-time availability and price for a specific hotel + room type + date range.
  • Book a room: hold inventory for ~15 min during payment, then commit on success or release on failure.
  • Cancel a booking (with policy: free >48h, 50% >24h, 0% <24h).
  • View/manage bookings; receive confirmation email/SMS.
  • Hotel staff can manage inventory, prices, room types, blackouts.

Non-functional requirements.

  • Search latency: p99 < 500 ms (searches span thousands of hotels).
  • Booking latency: < 2 s end-to-end.
  • Concurrency: handle two users racing for the last room — never double-book.
  • Availability: 99.95% (bookings are revenue).
  • Scale: 1M hotels, 50M room-nights sold per day, 10K searches/sec peak.

Non-goals. No dynamic pricing ML (rule-based pricing only in v1), no loyalty program, no multi-room bookings (single room per reservation in v1).

Capacity estimation.

Inventory size. 1M hotels x avg 5 room types x 365 days forward = 1.8B inventory rows (hotel_id, room_type_id, date, available_count, price). Each row ~50 bytes = 90 GB — fits in a sharded SQL DB.

Search traffic. 10K searches/sec peak. Each search filters hotels by city + date range + filters; returns ~100-1000 results. Read-heavy: 100x more searches than bookings.

Booking volume. 50M room-nights/day = ~5M bookings/day = ~60 bookings/sec. Each booking touches ~3 inventory rows (one per night) for a 3-night stay.

Holds. Every booking creates a temporary hold during payment. Holds expire after 15 min if payment doesn't complete. Peak concurrent holds ~10K.

Storage. Booking records ~1KB each = ~5 GB/day, ~2 TB/year. Audit trail (every state transition, who/when/why) ~5x = ~10 TB/year. Manageable in SQL with archiving.

APIs.

code
# Search
GET /v1/hotels?city=NYC&checkin=2025-01-10&checkout=2025-01-13&guests=2
  -> { hotels: [{id, name, stars, price_total, room_types: [...]}] }

# Hold inventory for a booking (15 min TTL)
POST /v1/holds
  { hotel_id, room_type_id, checkin, checkout, guest_count, idempotency_key }
  -> { hold_id, expires_at, price_total }
  -> 409 if no availability

# Confirm booking (after payment)
POST /v1/bookings
  { hold_id, payment_token, guest_info, idempotency_key }
  -> { booking_id, status: confirmed }

# Cancel
POST /v1/bookings/:id/cancel
  -> { status: cancelled, refund_amount }

# Get booking
GET /v1/bookings/:id

The two-step hold-then-confirm pattern is critical. Without holds, you'd either book without payment (open to abuse) or charge payment before checking availability (bad UX if it fails). The 15-min hold gives payment time to complete.

Data model.

Inventory (sharded SQL, sharded by hotel_id):

code
inventory (
  hotel_id, room_type_id, date,
  total_count, available_count, price_cents, version,
  PRIMARY KEY (hotel_id, room_type_id, date)
)

The version column is for optimistic locking. The available_count decrements when a hold is created and increments when released.

Holds (Redis, ephemeral):

code
hold:{hold_id}  -> { hotel_id, room_type_id, dates: [...],
                     guest_count, price_total, expires_at }
TTL = 900 (15 min)

Holds are in Redis (ephemeral) but the inventory decrement is in SQL (durable). On hold expiry, a sweeper increments available_count back.

Bookings (sharded SQL, sharded by booking_id):

code
bookings (
  id PK, user_id, hotel_id, room_type_id, checkin, checkout,
  status, price_cents, payment_id, created_at, cancelled_at,
  version,
  INDEX (user_id, created_at),
  INDEX (hotel_id, checkin)
)

Booking state machine:

code
searching -> holding -> payment_pending -> confirmed -> (checked_in -> checked_out -> completed)
                                  \-> failed (hold released)
                                  \-> cancelled (per policy)

Audit trail (append-only):

code
booking_events (booking_id, ts, event, actor, payload, PRIMARY KEY (booking_id, ts))

Every state transition appends an event — never updates. This is the event-sourcing pattern, applied narrowly for auditability.

Deep dive: concurrency control — pessimistic vs optimistic.

The race. Two users, Alice and Bob, both see the last remaining single-king room at the Marriott NYC for Jan 10-12. Both click 'book' simultaneously. The system must let exactly one succeed and reject the other.

Pessimistic locking: SELECT ... FOR UPDATE. When Alice's request enters the transaction, SELECT ... FOR UPDATE on the inventory rows (Jan 10, 11, 12 for that hotel+room_type) acquires row-level locks. Bob's SELECT FOR UPDATE blocks until Alice's transaction commits. After Alice commits (available_count = 0), Bob's SELECT sees 0 and returns 409. Simple and correct.

Pros: simple, no retry needed. Cons: lock contention under high concurrency — if 100 users race for the last room, 99 of them block; throughput collapses. Locks held during payment = bad (15 min).

Optimistic locking: version column. No lock acquired. Read inventory with version V. Check available_count >= 1. Then UPDATE inventory SET available_count = available_count - 1, version = version + 1 WHERE ... AND version = V. If the rowcount is 0, someone else changed it — retry the whole transaction (or fail).

Pros: no lock contention, scales much better under contention. Cons: retry overhead on contention; need explicit version column.

Our choice: optimistic locking for the hold (short transaction), no lock held during payment. The hold transaction completes in <100ms — no lock during the 15-min payment window. This is critical: we never hold a row lock for the duration of a payment.

Inventory at the date grain. Don't have one row per hotel+room_type with a 'dates_booked' array — you can't atomically check 'is Jan 10-12 all free' without scanning. Instead, one row per (hotel, room_type, date) and a transaction that locks/updates all rows in the range. SQL's row-level locks make this efficient.

Deep dive: overbooking and holds.

Overbooking math. Airlines and hotels deliberately sell more seats/rooms than they have, betting that some customers will no-show. If a hotel has 100 rooms and historical no-show rate is 5%, they can sell 105 rooms and expect 100 to show up. The expected cost of an oversell (rebooking a guest at another hotel + apology) is lower than the expected cost of an empty room.

Implementation. The total_count column is the physical count (100). The overbookable_count is the saleable count (105). Holds decrement from available_count, which can go below total_count - overbookable_count (i.e. we can oversell by up to overbookable_count - total_count = 5). When available_count < 0 we are in overbook territory; the search still shows availability but a flag is set so ops can intervene.

Walk rate management. 'Walk' = relocating an overbooked guest to another hotel. Ops dashboards show overbook count per hotel + date; if projected walks exceed threshold, ops stop new bookings or call partner hotels. The math is statistical — actual no-show rate varies; ops intervene on exceptions.

Holds and abandonment. ~30% of holds expire without payment — users change their mind, payment fails, etc. During those 15 min, the room appears unavailable to others. This is intentional (we want to honor the hold), but means effective availability is lower than the inventory count. Overbooking compensates for this: if 30% of holds expire, sell ~30% more than physical capacity.

Idempotent holds. A user might double-click 'book' or retry after a network blip. The hold API requires an idempotency_key; the Inventory Service stores idempotency_key -> hold_id in Redis for 24h. Duplicate requests within 24h return the original hold_id. Same pattern as Stripe.

Cancellation policies. Cancellations are not just 'set status to cancelled' — they have a refund policy. Free >48h before checkin, 50% >24h, 0% <24h. The Cancellation Service computes refund based on policy, calls Payment Service to issue refund, increments inventory.available_count back, publishes 'booking_cancelled' event.

Bottlenecks and failure modes.

  • Hot hotel contention. A popular hotel during a festival has 1000 users racing for 50 rooms. Mitigation: optimistic locking with bounded retries (3); after 3 retries, return 'temporarily unavailable, please try again' rather than infinite retry storms.

  • Hold expiration storms. A flash sale ends; 10K holds expire simultaneously. Mitigation: sweep in batches over 5 min, not all at once; rate-limit inventory increments.

  • Payment service timeout. Payment intent takes 30+ seconds (3DS challenge). Hold expires before payment completes. Mitigation: extend hold TTL when payment is in-flight; or use a 'payment_in_progress' state distinct from 'holding'.

  • Cross-shard transactions. A booking touches inventory (sharded by hotel_id) and bookings table (sharded by booking_id) — different shards. Mitigation: don't use distributed transactions; use the saga pattern — first decrement inventory (commit), then create booking (with hold_id link). If booking fails, compensate by incrementing inventory back.

  • Search query cost. 'Available hotels in NYC for Jan 10-12' must check inventory for all NYC hotels x all dates. Mitigation: precompute availability per hotel for the next 365 days; index by (city, available_count > 0, date_range); cache search results in Redis for 30s.

  • Time zone bugs. A 'date' is ambiguous without timezone. NYC midnight vs UTC midnight differ. Mitigation: always store dates in the hotel's local timezone; explicitly model check-in time as hotel-local.

  • Stale cache in search. Search results cached 30s; a hotel might sell out in those 30s. Mitigation: when user clicks 'book', the hold call validates against the DB (not the cache) and returns 409 if unavailable — the user sees 'sorry, just booked'.

Scaling strategy and trade-offs.

Inventory sharding. Shard by hotel_id. A hotel's inventory for all dates lives on one shard — enables a single-shard transaction for a booking. 1M hotels / 100 shards = 10K hotels per shard.

Search read replicas. Search is read-heavy (100x bookings). Read replicas for search; writes go to primary.

Precomputed availability. Daily job computes 'is hotel X available for date range Y' for the next 90 days, stores in a denormalized table for fast search. Avoids the per-hotel inventory scan on every search.

Multi-region. Hotels are inherently regional (a Tokyo hotel is searched mostly by users in Asia). Deploy per-region with the inventory for that region's hotels. Cross-region replication for global search.

Trade-offs made explicit.

  • We chose optimistic locking for holds — gained high concurrency under contention, lost simplicity (must retry on version conflict).
  • We chose 15-min hold TTL — gained payment time, lost effective availability during the hold window (mitigated by overbooking).
  • We chose overbooking at the saleable_count level — gained revenue optimization, lost the risk of walking a guest (managed by ops dashboards).
  • We chose saga over 2PC for cross-shard booking — gained availability (no global coordinator), lost the need for explicit compensation logic on failure.
  • We chose date-grain inventory (one row per night) — gained atomic multi-night checks, lost some storage (3 rows for a 3-night stay vs 1) — acceptable.
  • We chose 30s search cache TTL — gained search throughput, lost 30s of staleness (acceptable; hold call validates against DB).
Check yourself
interview

Two users race to book the last single-king room at the Marriott NYC for Jan 10-12. How do you guarantee only one wins?

Pick one answer.

Check yourself
interview

Why do hotels deliberately overbook (sell more rooms than they physically have)?

Pick one answer.

Engineering mental model

Mental model. Think of Design Hotel Reservation as a deliberate boundary in a system. The boundary exists because something becomes harder to manage when everything is done in one place: latency, scale, failure isolation, consistency, cost, or team ownership. The useful question is not “what does Design Hotel Reservation mean?” but “what pressure makes this boundary worth introducing, and what new failure mode does it create?”

Design lens

Before choosing Design Hotel Reservation, name the workload, the critical user path, the dominant bottleneck, the failure you are trying to absorb, and the trade-off you are willing to accept. If you cannot name those five things, the design is probably premature.

Original NO CAP systems visual for Design Hotel Reservation.
Image unavailable. Original NO CAP systems visual for Design Hotel Reservation.
Design Hotel Reservation: a compact system-thinking visual.— Original NO CAP visual.
// Pseudocode
request = receive()
result = design_hotel_reservation(request)
return result

// Production questions:
// 1. What happens on timeout?
// 2. Can this operation be retried safely?
// 3. What is the bottleneck?
A minimal engineering sketch for reasoning about Design Hotel Reservation.

Back-of-the-envelope reasoning

Numerical lens: write down traffic, payload size, read/write ratio, peak multiplier and durability target before choosing a component. The numbers should justify the architecture.

Interactive sandboxdeterministic

Interactive thought experiment: Design Hotel Reservation

Change the variables below and predict what breaks first in Design Hotel Reservation. The production lab can later reuse these same inputs.

System pressure6%
Try this

Change one variable at a time. Predict the failure mode first, then move the slider and see whether your mental model matches the simplified system response.

Hint

If you are stuck on Design Hotel Reservation, start by drawing the request path and marking every network hop, stateful component, queue, cache and failure boundary. Then estimate where the system will saturate.

Check yourself
solid

You increase traffic by 10× in a system using Design Hotel Reservation. What should you inspect first?

Pick one answer.

Check yourself
interview

Which statement is the safest engineering habit when using Design Hotel Reservation?

Pick one answer.

Try this
interview

You have dashboards for traffic, latency, errors and saturation. You can change the architecture, but every change has operational cost.

Production scenario: your system uses Design Hotel Reservation, traffic suddenly spikes, and p99 latency doubles. What is your first move?

Interview drill

Answer this without notes: When would you choose Design Hotel Reservation, and when would you intentionally avoid it? Mention at least one bottleneck it addresses, one failure mode it introduces, and one alternative. Then quantify the workload you are designing for.

Engineering lens

A useful engineering lens for Design Hotel Reservation: define the problem it solves, the simpler design that fails first, the constraint that forces you to introduce this concept, and the new failure modes the concept creates.

Numerical sanity check

Back-of-the-envelope reasoning beats fake precision. State your traffic, payload, concurrency and growth assumptions explicitly, then calculate enough to know whether the current architecture is orders of magnitude away from the target.

Check yourself
interview

Do not optimize for a memorized definition. Reason from the workload and failure mode.

Imagine the simplest version of a system using Design Hotel Reservation. What breaks first as traffic grows by 10×, and what would you change before reaching 100×?

Pick one answer.

Trade-offs

What you gain, what you pay

Pros
  • +Optimistic locking scales well under contention without holding locks during payment.
  • +Date-grain inventory enables atomic multi-night availability checks.
  • +Hold-then-confirm pattern separates payment latency from inventory commitment.
  • +Overbooking at saleable_count level optimizes revenue while bounding walk risk.
Cons
  • −Optimistic locking requires retry logic on contention — added complexity.
  • −15-min holds reduce effective availability (~30% expire, mitigated by overbooking).
  • −Saga over 2PC for cross-shard booking requires explicit compensation on failure.
  • −Date-grain inventory means 3 rows per 3-night booking — more storage, more rows touched per transaction.
Failure modes

How this breaks in production

  • Hot hotel contention during festivals — needs bounded retries + 'try again' UX.
  • Hold expiration storms from flash sales — needs batched sweep and rate-limited inventory increments.
  • Payment timeout outlasting hold TTL — needs 'payment_in_progress' state with extended TTL.
  • Cross-shard booking failure (inventory decremented but booking insert fails) — needs saga compensation.
  • Search query cost across thousands of hotels — needs precomputed availability and 30s cache.
  • Time zone bugs in 'date' columns — needs explicit hotel-local timezone.
  • Stale search cache shows unavailable rooms — needs hold call to validate against DB.
Common mistakes

Don't fall into these traps

  • •Holding a row lock during the 15-min payment window — kills concurrency and availability.
  • •Single row per hotel+room_type with a 'booked_dates' array — cannot atomically check multi-date availability.
  • •Using 2PC across inventory and booking shards — availability and throughput collapse.
  • •Booking before payment — open to abuse; or charging before availability check — bad UX.
  • •No idempotency key on hold creation — double-click 'book' creates two holds.
  • •Storing dates without timezone — midnight ambiguity causes off-by-one bookings.
  • •Overbooking without ops dashboards for walk rate — guest-walking incidents surprise ops.
Where you see it

Real systems using this

Booking.com (millions of bookings/day, this exact architecture)Expedia / Hotels.comAirbnb (listing availability with date cube + holds)Marriott / Hilton direct-booking sitesAirline seat inventory (same pattern with seat maps)OpenTable (restaurant reservation with hold-then-confirm)Ticketmaster (event ticket holds)
Teardowns

How real systems implement this

  • Booking.com — Inventory cube sharded by hotel, optimistic locking for holds, precomputed availability for search, overbooking at the property level. Documented in Booking.com engineering talks on their inventory system.
  • Airbnb — Listing availability modeled per-night (similar date-cube), hold-then-confirm during booking, optimistic locking. Cancellation policies encoded per listing.
  • Airline reservation systems (Amadeus, Sabre) — Same fundamental pattern: inventory cube (flight, class, date), hold-then-confirm, overbooking. Decades older than the hotel version but identical in pattern.
Interview prompts

Practice saying it out loud

  • Q1Design a hotel reservation system. How do you handle two users booking the last room?
  • Q2Why do airlines overbook flights? How would you implement overbooking safely?
  • Q3A user clicks 'book' but their payment takes 30 seconds. What happens to the room during that time?
  • Q4How do you handle the case where a hold expires but the user's payment then succeeds?
  • Q5Your search returns 1000 hotels. How do you make this fast?
Research

Further reading & references

System Design Primer
Open source
ByteByteGo — Scale from zero to millions
ByteByteGo
System Design Tutorial
GeeksforGeeks
System Design Roadmap
roadmap.sh
Case Studies reference
Reference
Case Studies reference
Reference
Case Studies reference
Reference

Core explanations are original NO CAP material. External references are provided for deeper study and standards.

What next?

Mark as understood once the mental model clicks.

Next recommended

Transactions & ACID