Designing a Hotel Reservation System
Design a hotel reservation system that manages room-night inventory, controls overbooking and keeps channel partners and pricing consistent.
Hotels sell perishable inventory: a room-night that is unsold at midnight is gone. A reservation system must show accurate availability and price across many channels, accept bookings concurrently, and never promise more rooms than policy allows.
Requirements
Functional
- Search availability by city, dates and guests; view rates.
- Book, modify and cancel reservations; support holds during payment.
- Sync inventory and rates with channel managers and OTAs.
- Manage room types, rate plans and restrictions such as minimum stay.
Non-functional
- Search p99 under 400 ms; booking p99 under 1 s.
- No unintended overbooking; controlled overbooking where policy allows.
- 99.95% availability.
- Search reads outnumber bookings by about 500 to 1.
Capacity Estimation
Assume 500,000 properties, an average of 100 rooms in 5 room types, and a 365-day booking window. Inventory rows are per property, room type and date: 500,000 times 5 times 365, about 900M rows. At 50 bytes each that is 45 GB, small enough to partition on a few nodes. Bookings: 1M per day is about 12 per second, peak 100. Search at 500x is 50,000 queries per second at peak, served mainly from a search index and cache.
High-Level Design
Guest -> CDN/API Gateway -> Search Service -> Search Index (Elasticsearch, cached rates)
-> Booking Service -> Inventory DB (partitioned by property)
| -> Payment Service
| -> Reservation DB
'-> Channel Manager Gateway <-> OTAs (queue-based sync)
Inventory/Rate changes -> Kafka -> Search Index updater, Channel sync, Analytics
Data Model
room_types(property_id, room_type_id, capacity, total_rooms)inventory(property_id, room_type_id, stay_date, total, booked, held, overbook_limit, version)rates(property_id, room_type_id, rate_plan_id, stay_date, price, min_stay, closed)reservations(reservation_id, property_id, room_type_id, checkin, checkout, guest_id, state, price_snapshot, idempotency_key UNIQUE)holds(hold_id, reservation_id, expires_at)
Hotels sell room types, not specific rooms, until check-in. Assigning an actual room number late gives flexibility and reduces conflicts.
Deep Dives
Concurrency over date ranges
A stay from the 10th to the 13th consumes three inventory rows. Booking updates each row with a conditional statement: increment booked where booked plus held plus requested is at most total plus overbook_limit. All three updates run in one transaction in date order, so a failure on any night rolls back the whole booking and ordering prevents deadlocks. Because inventory is partitioned by property, the transaction stays on one node.
Holds and two-step booking
A hold increments held and sets expiry, typically 10 to 15 minutes. On payment success the hold converts to booked. On timeout a sweeper decrements held. Availability checks always read booked plus held, with expiry enforced on the hold record, so a lagging sweeper only causes conservative under-selling for a short time.
Idempotency
Booking requests carry a key stored uniquely on the reservation. A retry after a network failure returns the existing reservation. Modification and cancellation use the reservation version for optimistic locking, preventing two agents from editing the same booking concurrently.
Overbooking
Hotels expect cancellations and no-shows, commonly 5 to 10%. The overbook_limit per date is set by a revenue-management model. When the hotel is truly full at arrival, the walk policy relocates guests, a business process the system must support by tracking relocation records.
Channel synchronization
OTAs hold their own cached view. Push inventory changes through a queue per channel with ordering by property and date. Use versions so older updates cannot overwrite newer ones. For channels that cannot receive pushes, expose pull APIs and tolerate rate limits. The central inventory is the source of truth, and rare conflicts are resolved by accepting the first confirmed booking and notifying the channel to reject others.
Search versus booking
Search uses a denormalized index refreshed within seconds from the change stream. It may show a room that was just sold. The booking step re-verifies against the inventory database and re-prices, so the search index is never authoritative.
Failure Modes and Trade-offs
- Search index lag: users occasionally see unavailable rooms; the booking step fails gracefully with alternatives.
- Payment failure after hold: the hold expires and releases inventory.
- Channel outage: queue buffers updates; reconcile nightly by comparing channel and central counts.
- Price changes between search and book: store a price snapshot and honor it for the hold window, or reconfirm with the user.
- Per-night rows versus availability ranges: per-night rows are simple and fast to update, while range structures save space but complicate concurrent edits.
- Overbooking versus guest experience: higher limits raise revenue and walk costs.
Key takeaways
- Model inventory as room-type counts per night, partitioned by property.
- Multi-night bookings are atomic transactions over date-ordered conditional updates.
- Holds with expiry protect inventory during payment without blocking correctness.
- Search is a cache; booking is authoritative and re-verifies.
- Versioned, ordered queues keep channels roughly in sync, backed by nightly reconciliation.
- Overbooking is a controlled business parameter, not a bug.