Booking systems are the counterweight to the social-feed questions: here **consistency matters more than availability**. Selling the same room twice is a real-world failure, not a stale cache. Say that early — it is the framing the whole answer hangs on.
Requirements
Functional
- Search hotels by location, dates and guests.
- See available room types and prices.
- Book, pay, modify, cancel.
- Manage inventory across many properties.
Non-functional
- No double booking. Strong consistency on the reservation path.
- Search must be fast and can be eventually consistent.
- Availability is read far more than it is written.
100K hotels, ~100 rooms each → 10M rooms
Searches: 10,000/second Bookings: ~100/second
Read:write ≈ 100:1 — but the writes are the ones that must be exactly rightModelling inventory
The critical modelling insight: do not book a specific room. Hotels sell room types, and assign a physical room at check-in. Reserving room 412 for a week creates false conflicts and fragments availability for no benefit.
room_inventory
hotel_id | room_type_id | date | total | booked
1 | deluxe | 2024-07-14 | 20 | 18
1 | deluxe | 2024-07-15 | 20 | 20One row per room type per night. A three-night stay touches three rows, and availability
is "every night in the range has booked < total". This shape makes the concurrency
control tractable and partial-availability queries natural.
Preventing double booking
The heart of the question. A naive check-then-write is a race:
User A: SELECT booked (18 of 20) → ok
User B: SELECT booked (18 of 20) → ok
User A: UPDATE booked = 19
User B: UPDATE booked = 19 ← one booking vanishedPessimistic locking — lock the inventory rows for the duration of the transaction:
BEGIN;
SELECT total, booked FROM room_inventory
WHERE hotel_id = 1 AND room_type_id = 'deluxe'
AND date BETWEEN '2024-07-14' AND '2024-07-16'
FOR UPDATE; -- other transactions wait here
UPDATE room_inventory SET booked = booked + 1 WHERE …;
INSERT INTO reservations …;
COMMIT;Correct and simple. The cost is contention on popular hotels and dates. Always lock rows in a consistent order (by date) or two overlapping multi-night bookings can deadlock.
Optimistic locking — no locks; check on write and retry:
UPDATE room_inventory
SET booked = booked + 1
WHERE hotel_id = 1 AND room_type_id = 'deluxe'
AND date = '2024-07-14'
AND booked < total; -- the guard IS the concurrency control
-- 0 rows affected ⇒ someone else took the last room ⇒ fail the bookingBetter under low contention, and the booked < total predicate makes overselling
impossible regardless of interleaving. A CHECK (booked <= total) constraint is the belt
and braces.
Recommend optimistic with a database constraint as the backstop, and mention that pessimistic is the right call for genuinely scarce inventory such as a flash sale.
Holds
Users need time to enter payment details, and holding a database lock during a human's typing is unacceptable.
1. Select a room → create a HOLD with expires_at = now + 10 minutes
(counts against availability, so nobody else can take it)
2. Payment succeeds → convert the hold into a confirmed reservation
3. Payment fails or the timer expires → release the holdExpiry needs both a background sweeper and a lazy check at read time — a sweeper alone leaves stale holds visible until it next runs. This two-phase pattern is exactly how concert tickets and airline seats work too.
Search
Search is a different system from booking, and should be read-optimised and eventually consistent.
Elasticsearch: hotel documents with geo coordinates, amenities,
price ranges, ratings, and denormalised availability summaries
Query: geo filter → facet filters → rank by relevance/price/rating
Then: verify true availability for the few results shown, against the
inventory databaseThe two-step matters: the index can be seconds stale, so a hotel may appear available and not be. Verifying only the displayed page keeps the authoritative check cheap and the search fast.
Payments and idempotency
The booking and the charge must not diverge. Use an idempotency key per booking attempt so a client retry after a timeout cannot charge twice — see payment gateway. Because the payment provider is external, the sequence is typically: authorise, confirm the reservation, then capture, with a saga to compensate (release inventory, void the authorisation) if any step fails.
Deliberate overbooking
Real hotels overbook, because a predictable percentage of guests do not show. Model it as
total = physical_rooms × (1 + no_show_rate) per date, with a walk policy when the
prediction is wrong. It is worth raising unprompted: it shows you understand that the
business rule, not the database, defines correctness here.
Follow-ups to expect
- Sharding — by
hotel_id, so a booking transaction stays on one shard. - Dynamic pricing — a separate service; the price is quoted and locked into the hold.
- Cancellation policy — refund rules, and returning inventory to the pool.
- Multi-room bookings — all or nothing across room types, in one transaction.
- Group and corporate rates — inventory allocations reserved per channel.
- Cross-channel inventory — the same rooms sold on aggregators; a channel manager
syncs availability, and the reconciliation lag is a real source of overselling.
Track this problem on the System Design sheet.
Frequently asked
How do you prevent double booking a hotel room?
Model inventory as one row per room type per night with total and booked counts, then make the decrement conditional: UPDATE … SET booked = booked + 1 WHERE booked < total. If zero rows are affected, someone else took the last room. A CHECK constraint that booked never exceeds total is the backstop. Pessimistic SELECT … FOR UPDATE also works and is preferable under very high contention, but requires consistent lock ordering to avoid deadlocks.
Why book a room type instead of a specific room?
Because hotels sell room types and assign physical rooms at check-in. Reserving room 412 specifically creates false conflicts and fragments availability — a night blocked in the middle of a range can make an otherwise bookable stay look unavailable. Counting availability per type per night avoids all of that.
How do you hold inventory while a user enters payment details?
Create a hold row with an expiry a few minutes out that counts against availability, then convert it to a confirmed reservation when payment succeeds or release it on failure or timeout. Holding a database lock during a human's typing is unacceptable. Expiry needs both a background sweeper and a lazy check at read time, or stale holds stay visible between sweeps.