Case study: Hotel booking
Design a hotel reservation system with fast cached availability search, atomic room inventory holds, and idempotent checkout without overselling.
35 min9 steps
By the end you can
- Estimate room-night inventory storage, browse-to-book traffic ratio, and peak checkout rate
- Model nightly room availability and reservation state transitions to prevent double-booking and deadlocks
- Guarantee zero overselling under flash-sale concurrency using atomic conditional decrements and idempotency keys
- Predict how availability caching, hold expiry reapers, and database failover behave under failure
Separate the read flood from the transactional core
Browse
- Availability/rates for [checkIn, checkOut); p99 <150 ms
Reserve
- Timed holds, payments, confirmation/cancellation; p99 <500 ms
Safety
- Strict room-night transactions; no accidental oversell/duplicate charge; browse may lag seconds
Size the inventory and the traffic
Inventory
9.1B rows
5M hotels * 5 types * 365; 64 B gives 580 GB
Bookings
46/s
4M/day; peak about 250/s; 750 nightly updates/s
Browse
25k reads/s peak
100:1 browse/book; 10-15 s cache absorbs >90%
Design an idempotent booking API
Browse
GET /v1/hotels/{id}/availability?checkIn=&checkOut=; 15 s cache
Hold
POST /v1/reservations + Idempotency-Key -> 201 with expiresAt or 409
Cancel
DELETE /v1/reservations/{id}; atomic status + restore all nights
Model per-night inventory counters
Inventory
hotelId + roomTypeId + date -> available, priceCents
Reservation
reservationId -> guest, dates, HOLD/CONFIRMED/CANCELLED/EXPIRED, expiresAt
Shard
hotelId keeps every stay night in one local transaction
Put the read cache and transactional checkout together
Order the steps of a successful reservation request on the checkout path.
- 1Execute an atomic conditional decrement (available > 0) on the hotel's room-date inventory rows in ascending date order
- 2Insert the reservation record and replicate to a semi-synchronous standby before committing
- 3Return 201 Created with the reservationId to the guest
- 4Verify the client's Idempotency-Key in the dedup store to ensure this checkout has not already run
Deep dive: holds, payment sagas, and overbooking
Hold
- Short transaction; decrement nights; HOLD expires in 10 minutes
Pay
- Outside locks; reservationId is payment retry key
Resolve
- Conditional HOLD -> CONFIRMED or EXPIRED; restore nights once
Overbook
- Explicit capacity allowance; never weaken isolation
Compare alternative concurrency designs
Read then write
- Concurrent readers oversell last room
No cache
- Browse saturates DB; goodput <85%
Version CAS
- Safe but hot rooms generate retries
Conditional decrement
- WHERE available >0 serialises valid bookings
Run the failure scenarios
Inject
25% traffic targets 2 remaining rooms
Observe
Cache absorbs searches
Recover
2 bookings succeed; rest 409; no oversell
Inject
Empty cache at 1.5 s
Observe
Coalesce same-hotel misses
Recover
Single DB refill; requests continue
Inject
DB crashes at 1.5 s
Observe
96% cached browsing continues
Recover
About 0.5 s promotion; semiSync + fencing protect reservations
Wrap up and score your design
Guard
All nights atomically; idempotent booking/payment
Separate
Cached browse; payment outside locks
Recover
Expire holds once; fence old leaders
Answer this step to continue.