System Design · Design Question
Design a Booking System
Hotels / tickets / appointments: inventory, holds, payments, and how to avoid overselling under concurrency.
1. Requirements
- Search availability for a resource (room type / seat / slot) in a date range.
- Hold inventory briefly while user pays; confirm or release.
- No double booking of the same unique unit (or controlled overbook policy).
- Cancel/refund flows; idempotent booking APIs.
Pick a concrete domain in interview: "hotel room nights"
Ask: unique rooms vs pooled room-type inventory? overbooking %?
2. API
GET /v1/availability?hotelId=&checkIn=&checkOut=&guests=
POST /v1/holds
{ hotelId, roomTypeId, checkIn, checkOut, customerId }
→ { holdId, expiresAt, price }
POST /v1/bookings
{ holdId, paymentMethod, Idempotency-Key }
→ { bookingId, status }
POST /v1/bookings/{id}/cancel
3. Data model
hotels, room_types(room_type_id, hotel_id, capacity_attrs)
inventory(room_type_id, date, total, reserved, held)
-- reserved = confirmed; held = soft lock
holds(hold_id, room_type_id, dates, expires_at, status)
bookings(booking_id, hold_id, user_id, status, amount)
UNIQUE constraints / TX protect inventory rows
For assigned seats/rooms, model each unit; for hotels often count-based inventory per room type per night.
4. High-level design
Client → API Gateway
→ Search Service (read replicas / cache)
→ Booking Service → Inventory DB (strong consistency path)
→ Payment Service
→ Notification queue
Hold Expiry Worker releases expired holds
5. Concurrency: do not oversell
-- In one DB transaction:
SELECT total, reserved, held FROM inventory
WHERE room_type_id=? AND date IN (...) FOR UPDATE
IF any day reserved+held+need > total → abort
UPDATE inventory SET held = held+need ...
INSERT hold expires_at = now()+10min
COMMIT
- Pessimistic:
FOR UPDATEon inventory rows — simple, correct. - Optimistic: version column; retry on conflict — good under low contention.
- Redis locks: possible for holds; still commit inventory in DB as source of truth.
6. Hold → pay → confirm saga
1) Create hold (inventory held++)
2) Charge payment (idempotent)
3) On success: held-- , reserved++ ; booking confirmed
4) On fail/expiry: held-- ; hold cancelled
5) Payment success but confirm crash → reconcile job uses payment id / hold id
7. Search scaling
- Availability reads are heavy — cache popular hotel/date pages with short TTL.
- Accept brief stale “available” then fail at hold time (honest UX).
- Do not use cache as authority for the hold transaction.
8. Overbooking (optional product)
Airlines intentionally overbook statistically. If required, raise
total effective capacity and define compensation workflow —
say it explicitly; do not “accidentally” oversell.
9. Scaling writes
- Shard inventory by hotel_id.
- Hot hotel + hot dates → row contention; consider coarser locks or queue per hotel.
- Payment and notify async after confirm.
10. Edge cases
- Clock skew on hold expiry → use DB time.
- Partial multi-night failure → all-or-nothing nights in one TX.
- Double submit → Idempotency-Key on booking create.
Quick revision
- Separate search (cacheable) from hold/book (strongly consistent TX).
- Hold with TTL; worker releases expiry.
- Protect inventory with row locks or optimistic versions.
- Saga: hold → pay → confirm; compensate on failure.
- Idempotency keys on booking and payment.
- Shard by hotel; watch hot-date contention.
- Overbooking only as an explicit policy, not a race bug.