Hotel booking (Booking.com / Airbnb)
Browse availability across millions of rooms and book a stay for a date range. Browsing is 95%+ of traffic and tolerates a few seconds of staleness; booking is a small transactional core where two guests must never claim the same last room and a retried checkout must never book twice.
Component labels and connections
- Clients (client) · calls: Load balancer
- Load balancer (loadBalancer) · calls: Booking API 1, Booking API 2
- Booking API 1 (service) · calls: Availability cache, Idempotency store, Inventory & reservations DB
- Booking API 2 (service) · calls: Availability cache, Idempotency store, Inventory & reservations DB
- Availability cache (cache) · calls: none
- Idempotency store (dedupStore) · calls: none
- Inventory & reservations DB (database) · calls: none
Requirements
Functional
- Search hotel room types and view nightly availability and rates for a check-in / check-out range.
- Place a reservation hold on a room type for every night in the stay, complete payment, and confirm or release the hold.
- View or cancel an existing reservation (returning inventory to the pool).
Non-functional
- Zero overselling beyond configured overbooking margin: concurrent checkouts for the last room must grant at most one reservation.
- Exactly-once booking semantics on client retries via an idempotency key.
- Availability search p99 under ~150 ms; booking checkout p99 under ~500 ms.
- Browse path stays highly available even if the transactional booking store is failing over.
Worked estimates
Inventory rows
≈ 9.1B rows × ~64 B ≈ 580 GB — fits comfortably in a sharded relational store
5M hotels × 5 room types × 365 bookable days
Bookings
≈ 46 bookings/s average, ≈ 250/s peak — small enough for ACID transactions
4M bookings/day ÷ 86,400
Searches & page views
≈ 25k availability reads/s at peak, absorbed by a short-TTL cache
100:1 browse-to-book ratio × 250/s peak
Lock contention
serialised on one (hotelId, roomTypeId, date) row for ~3 ms each — atomic conditional update rejects extras immediately
popular convention hotel: 50 users clicking 'Book' in the same second
Hold expiry
≈ 150k active holds at peak — tiny state for a background reaper
250 holds/s × 600 s (10-minute checkout window)
API
GET /v1/hotels/{id}/availability?checkIn&checkOut
Request: ISO dates, guests
Response: 200 { roomTypes[{ roomTypeId, available, priceCents }] }
Served from a short-TTL cache; slightly stale counts are fine until checkout.
POST /v1/reservations
Request: { hotelId, roomTypeId, checkIn, checkOut, guest } + Idempotency-Key
Response: 201 { reservationId, status: 'CONFIRMED' } · 409 sold out
DELETE /v1/reservations/{id}
Request: —
Response: 200 { status: 'CANCELLED' }
Atomically transitions status and increments inventory for each night.
Data model
- inventory — store: bookingdb
- reservations — store: bookingdb
inventory stores one row per (hotelId, roomTypeId, date) rather than interval ranges: a 3-night stay updates 3 rows in primary-key order (to avoid deadlocks) with `available = available - 1 WHERE available > 0`.
reservations is keyed by client-supplied or server-generated reservationId and tracks the state machine (PENDING → CONFIRMED → CANCELLED / EXPIRED).
Sharding by hotelId keeps all nights and room types for a booking inside a single shard transaction.
High-level design
Trace permitted calls in the architecture above, then open the reference canvas to inspect routing.
Deep dives
Per-night inventory counters vs interval locking
Hotels sell room categories ('Deluxe King'), not physical room numbers, at booking time; room numbers are assigned at check-in. That lets you model inventory as one row per `(hotelId, roomTypeId, date)` with an `available` integer. Booking a 3-night stay runs a single transaction updating the 3 rows in ascending date order (ordering prevents deadlocks when two guests book overlapping ranges) with `UPDATE inventory SET available = available - 1 WHERE hotel_id = ? AND room_type_id = ? AND date = ? AND available > 0`. If fewer than 3 rows match, the transaction rolls back and returns 409.
Two-step holds and payment sagas
Never hold a database transaction open while calling an external payment gateway. Instead, step 1 atomically decrements `available` and inserts a `reservations` row with `status = 'HOLD'` and `expiresAt = now + 10 min`. Step 2 charges the card outside the DB lock using `reservationId` as the payment idempotency key. Step 3 marks the reservation `CONFIRMED`. A background reaper scans expired `HOLD` rows every minute, marks them `EXPIRED` with `WHERE status = 'HOLD'`, and increments `available` back.
Overbooking as a data problem, not a race condition
Hotels intentionally overbook by 5–10% to account for no-shows. Model this explicitly by storing `totalCapacity`, `overbookingAllowance`, and `available = totalCapacity + overbookingAllowance - booked`, rather than relaxing transaction isolation. Correctness still requires atomic updates so the controlled cap is never exceeded by concurrent requests.
Failure scenarios
Conference hotel flash sale: 25% of traffic targets one room type
Scenario and expected result
A major conference announces its venue and a quarter of all searches and bookings target the same hotel room key, which only has 2 rooms left.
Expected: The availability cache absorbs the search flood. On the booking path, the first 2 reservations decrement `available` to 0 and succeed; every subsequent checkout gets 409 Sold Out from `decrementIfPositive`. Zero overselling occurs (compare the read-then-write alternative).
The availability cache restarts empty
Scenario and expected result
At t = 1.5 s the availability cache restarts, dropping all cached hotel availability pages.
Expected: Popular hotels refill within tens of milliseconds thanks to request coalescing on the cache and spare database capacity. Bookings are unaffected.
The booking database leader crashes mid-day
Scenario and expected result
The primary booking database crashes at t = 1.5 s; missed heartbeats and promotion take ~0.5 s.
Expected: Cached availability pages continue serving 96% of users without interruption. Checkouts and uncached searches fail briefly during leader election, then resume on the promoted replica with fencing and semi-synchronous replication preserving every confirmed booking.
Alternative designs
Reference
- Inspect the architecture above and run the same scenario.
Read-then-write room inventory without atomic bounds check
- Reading `available`, checking `> 0` in application code, and then decrementing works under light traffic, but when a conference hotel goes on sale, concurrent checkouts all read `available = 1` and all decrement it below zero. Run the hot-hotel scenario against this alternative to see `oversell` violations.
Reference
- Inspect the architecture above and run the same scenario.
Unindexed availability scan without cache
- Always shows exact live room counts on the search page, but sends the 96% browse workload straight to the transactional database without a cache or hotel index. In the simulated cell, scanning the inventory table on every search saturates the database worker pool and pushes p99 latency above 25 ms (more than double the cached design).
Reference
- Inspect the architecture above and run the same scenario.
Store bookings as date intervals with exclusion constraints instead of per-night rows
- Necesary when individual physical rooms are assigned at booking time (Airbnb listings, specific seats), using PostgreSQL range types (`daterange`) or per-day bitmaps. For hotels selling interchangeable rooms of a category, per-night counter rows are much simpler and faster to update.
Clarifying questions
- Do guests book a room category (Deluxe King) or a specific physical unit (like Airbnb)?
- What is the browse-to-book ratio, and can search results be a few seconds stale?
- Do we need a temporary hold window while the user enters payment details?
- Should the system support controlled overbooking margins per hotel?
Common mistakes
- Reading `available` into application memory and writing `available - 1` without an atomic conditional check or row lock.
- Holding a database transaction open across a slow third-party payment gateway call.
- Using `SERIALIZABLE` isolation across the entire database instead of row-level conditional updates.
- Skipping the availability cache because 'inventory must be strongly consistent', overloading the DB with browse traffic.
- Retrying timed-out booking requests without an `Idempotency-Key`, booking and charging the guest twice.
Self-assessment rubric
requirements / levels 1-4
- 1/4 Search and book rooms.
- 2/4 Mentions no double-booking.
- 3/4 Separates read-heavy browse SLOs from strict transactional checkout and idempotency.
- 4/4 Clarifies room-type vs specific-room inventory, hold expiry, and intentional overbooking.
estimates / levels 1-4
- 1/4 None.
- 2/4 Booking QPS only.
- 3/4 Computes ~9B inventory rows (~600 GB) and ~100:1 browse-to-book ratio.
- 4/4 Shows booking QPS (~50–250/s) easily fits sharded SQL while browse needs caching.
api / levels 1-4
- 1/4 GET and POST endpoints.
- 2/4 Includes dates and room type.
- 3/4 Idempotency-Key on POST and 409 on sold-out conflict.
- 4/4 Models hold creation, confirmation, and cancellation state transitions.
data model / levels 1-4
- 1/4 Rooms table.
- 2/4 Bookings table with start/end dates.
- 3/4 Per-(hotel, roomType, date) inventory counter rows + reservations table.
- 4/4 Shards by hotelId and sorts multi-night updates by date to prevent deadlocks.
high level / levels 1-4
- 1/4 API + DB.
- 2/4 Adds cache for search.
- 3/4 Cache-aside for availability, dedup store, and replicated DB with atomic updates.
- 4/4 Decouples payment gateway calls from DB locks using time-bounded reservation holds.
deep dive / levels 1-4
- 1/4 None.
- 2/4 Mentions locking.
- 3/4 Compares pessimistic `SELECT FOR UPDATE`, atomic conditional decrement, and optimistic CAS.
- 4/4 Explains hold reaper races, cache invalidation vs TTL, and deadlock prevention.
wrap-up / levels 1-4
- 1/4 Stops.
- 2/4 Mentions monitoring.
- 3/4 Identifies hot-hotel flash sales and abandoned holds as key risks.
- 4/4 Defines metrics on hold conversion, 409 conflict rate, and inventory reconciliation audits.