Design a Hotel Booking System
You're asked to design a hotel reservation system like Booking.com or Marriott's own platform. A user searches for a city and a set of dates, sees which room types are available at what price, and books one. Behind that, hotels manage their inventory, prices change constantly, and the same room must never be sold to two guests for overlapping nights.
Unlike a ticket, a hotel booking occupies a range of nights. A room may be free Tuesday but booked Wednesday through Friday, so its availability depends on the dates in the request. Let's make the room-night inventory model explicit before designing the claim.
Clarify the requirements
- Do guests book a specific room or a room type? Usually they book a type, such as "king, non-smoking," and receive a physical room at check-in. Confirm this early because it simplifies the inventory model.
- How far ahead can bookings be made? A year or two is typical, and it sets how many nights of inventory you store per room type.
- Is overbooking allowed? Hotels may overbook to offset no-shows. Ask because the answer changes the claim logic.
- What does search have to support? City, dates, guest count, price, and amenities are common filters. Keep search separate from the authoritative booking path.
- Can bookings be modified? Treat changing dates as a cancel-and-rebook against a different set of nights.
Assume: room-type inventory, a two-year booking window, controlled overbooking, and modifications treated as rebooking.
Back-of-envelope numbers
- Inventory rows: of per-night availability
- At roughly 100 bytes per row: , which is large but very partitionable
- Searches: average, peaking several times higher
- Bookings: , so the write rate is small compared to search
The search-to-booking ratio is roughly fifty to one. Give search and booking separate stores because one serves high-volume approximate queries while the other performs low-volume exact transactions.
High-level architecture
- API gateway. Separates the high-volume search path from the low-volume booking path.
- Search service. Answers "hotels in this city with availability on these dates" from a denormalized index.
- Search index. Optimized for filtering and ranking, and allowed to be seconds behind reality.
- Booking service. The only writer to inventory, and the only component that decides availability authoritatively.
- Inventory DB. A row per room type per night, partitioned by hotel so a booking touches one partition.
- Payment provider. External, slow, and the reason a hold exists between selection and confirmation.
- Change stream. Propagates inventory changes to the search index.
Deep dive 1: modeling inventory as room-nights
A table of bookings with check-in and check-out dates makes every availability check an interval-overlap query. It also makes the atomic claim awkward because the system needs to lock every night in a range rather than one row.
Instead, store one row per room type per night with total and booked counts:
A three-night stay is then three rows, and availability is a straightforward comparison on each. Several useful properties follow:
- Search becomes a scan of a date range for a hotel rather than an overlap query against every existing booking.
- Per-night pricing falls out naturally, since a rate column on the same row handles weekends and seasons without any extra structure.
- Partial availability is expressible. A guest wanting four nights where only three are open gets a clear answer rather than a silent failure.
- The row count is bounded and predictable, growing with hotels and nights rather than with booking volume.
The cost is that a booking now writes several rows instead of one, which is what the next section deals with.
Deep dive 2: claiming a range atomically
A three-night booking must take all three nights or none. Taking two and failing on the third leaves inventory consumed for a stay that never happened.
Within one hotel this is straightforward, because partitioning by hotel puts every row for the booking in the same partition and therefore inside one database transaction. The claim is a conditional update across the range, and the affected row count tells you whether it succeeded:
SQLUPDATE inventory SET booked = booked + 1
WHERE hotel_id = ? AND room_type = ?
AND date >= ? AND date < ?
AND booked < total;
If that updates fewer rows than the number of nights requested, at least one night was full, and the transaction rolls back. The comparison between requested nights and affected rows is the whole check, and it happens inside the same statement that would have made the change.
Add two refinements:
- Consistent row ordering prevents deadlocks. Two overlapping bookings that lock nights in different orders can wait on each other, and always updating in date order removes that possibility.
- Holds still apply where payment is separate from booking. A
heldcount alongsidebooked, with an expiry, gives the same hold-then-confirm shape used for ticketing, applied to every night in the range.
Deep dive 3: search that can be stale, booking that cannot
Search asks a fundamentally different question from booking. It wants "roughly which hotels in Barcelona have a room in mid-August, ranked by price and rating," across hundreds of properties, and it runs constantly. Booking asks "is this specific room type available on these three specific nights," once, and the answer has to be exact.
Running search against the inventory database would put a hundred times the load on the component that has to stay correct, so search runs against a denormalized index that carries the hotel's attributes plus a compact availability summary. That index is updated from an inventory change stream and is allowed to lag by seconds.
The consequence is that a user can be shown a hotel that just sold out, and the design has to handle that gracefully rather than pretend it won't happen:
- Re-verify at selection. When the user picks a room, the booking service checks authoritative inventory before presenting the payment step, so the failure surfaces early rather than after they've entered a card.
- Bias the index toward false positives. Showing a hotel that turns out to be full is a mild annoyance; hiding one that's actually available loses a booking outright.
- Keep availability coarse in the index. Storing "has some availability in this date range" rather than exact counts keeps the index small and reduces how often it needs updating.
Deep dive 4: overbooking on purpose
Hotels sell more rooms than they have, because a predictable share of guests never arrive. A design that treats the physical room count as a hard ceiling is modeling the technology correctly and the business incorrectly.
Implementing it is a small change: the ceiling in the claim becomes a sellable limit rather than the physical count, so a twenty-room hotel might sell twenty-two on a night with historically high no-show rates. The limit is set per hotel and per night from historical data, and it belongs in the inventory row rather than in application logic.
Occasionally everyone shows up and the system has oversold. The handling is operational, walking the guest to a comparable hotel at the property's expense, but it also affects our design. The system needs to know which bookings to prioritize when a room isn't available, typically by loyalty tier and length of stay, and it needs to record the incident so the overbooking model can be corrected.
Common pitfalls
- Storing bookings as date ranges and checking for overlaps. Every availability query becomes an interval scan, and the atomic claim has no natural unit to operate on.
- Booking nights one at a time. A partial failure consumes inventory for a stay that never happens.
- Updating rows in inconsistent order. Overlapping bookings deadlock against each other under load.
- Booking directly against the search index. The index is stale by design, so it will oversell.
- Treating physical capacity as the sellable limit. It leaves revenue on the table that the business expects to capture.
Leveling signals
Related lessons
Sell assigned seats under extreme contention, with holds and a waiting room.
Search local businesses by proximity, with filters and aggregate ratings.