Skip to content
All case studies

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.

ClientsLoad balanc…Booking API…Booking API…Availabilit…Idempotency…Inventory &…
Arrows show permitted calls. Open the canvas to inspect route programs.
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

Non-functional

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 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).

Open this scenario

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.

Open this scenario

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.

Open this scenario

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.
Open this alternative

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).
Open this alternative

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

Common mistakes

Self-assessment rubric

requirements / levels 1-4
  1. 1/4 Search and book rooms.
  2. 2/4 Mentions no double-booking.
  3. 3/4 Separates read-heavy browse SLOs from strict transactional checkout and idempotency.
  4. 4/4 Clarifies room-type vs specific-room inventory, hold expiry, and intentional overbooking.
estimates / levels 1-4
  1. 1/4 None.
  2. 2/4 Booking QPS only.
  3. 3/4 Computes ~9B inventory rows (~600 GB) and ~100:1 browse-to-book ratio.
  4. 4/4 Shows booking QPS (~50–250/s) easily fits sharded SQL while browse needs caching.
api / levels 1-4
  1. 1/4 GET and POST endpoints.
  2. 2/4 Includes dates and room type.
  3. 3/4 Idempotency-Key on POST and 409 on sold-out conflict.
  4. 4/4 Models hold creation, confirmation, and cancellation state transitions.
data model / levels 1-4
  1. 1/4 Rooms table.
  2. 2/4 Bookings table with start/end dates.
  3. 3/4 Per-(hotel, roomType, date) inventory counter rows + reservations table.
  4. 4/4 Shards by hotelId and sorts multi-night updates by date to prevent deadlocks.
high level / levels 1-4
  1. 1/4 API + DB.
  2. 2/4 Adds cache for search.
  3. 3/4 Cache-aside for availability, dedup store, and replicated DB with atomic updates.
  4. 4/4 Decouples payment gateway calls from DB locks using time-bounded reservation holds.
deep dive / levels 1-4
  1. 1/4 None.
  2. 2/4 Mentions locking.
  3. 3/4 Compares pessimistic `SELECT FOR UPDATE`, atomic conditional decrement, and optimistic CAS.
  4. 4/4 Explains hold reaper races, cache invalidation vs TTL, and deadlock prevention.
wrap-up / levels 1-4
  1. 1/4 Stops.
  2. 2/4 Mentions monitoring.
  3. 3/4 Identifies hot-hotel flash sales and abandoned holds as key risks.
  4. 4/4 Defines metrics on hold conversion, 409 conflict rate, and inventory reconciliation audits.