System design interview
Design a hotel booking system
Requirements, estimates and a step-by-step design for a hotel booking system: room inventory, conditional updates, idempotent bookings, search, overbooking.
Press Play to follow a request through the design, or step through it.
- A traveller searches for a city, dates and number of guests.
- The gateway routes the search to the Search service.
- The service queries the search index for hotels that match and have a room for every night.
- The index answers from its cached counts and prices, which are at most a few seconds old.
- The service ranks the hotels and builds the result page.
- The traveller sees hotels and prices. Availability is advice, not a promise, until they book.
- Client
- API gateway
- Search service
- Booking service: hold, pay, confirm
- Search index: cached availability
- Inventory DB: rooms per night
- Payment provider: charges the card
On this page
In 2 minutes
I would separate searching from booking, because search is about a hundred times busier and can tolerate a few seconds of staleness, while booking must be exact. The source of truth is a relational inventory table with one row per room type per night, holding sellable and reserved counts. Our estimate gives 365 million rows, about 18 GB, so one database with replicas, sharded by hotel, is enough. To book, one transaction adds one to the reserved count for each night, only where reserved is still below sellable, and checks that every night was updated. If any night is full it rolls back. The database serialises the rows, so only one of two competing guests gets the last room. The booking starts as a pending hold that expires after about 10 minutes, then payment is taken with the same idempotency key, then it is confirmed. A job releases expired holds exactly once. Search reads a separate index that is updated from change events, and booking re-checks availability and price at the moment of purchase. Overbooking is just a higher sellable number, set by business policy.
The question
Clarifying questions
- Do guests book a room type, such as 'double with a view', or one specific room? Booking a type needs only a count per night. Booking a specific room turns it into a harder scheduling problem.
- Is payment taken at booking, or at the hotel? Taking payment adds a step that can fail or time out, so the room needs a hold with an expiry while payment is made.
- Are we allowed to overbook, as many hotels do for no-shows? Overbooking is a business policy that changes the hard limit from rooms owned to rooms sellable.
- How many searches happen for each booking? A high look-to-book ratio means search is the heavy load, and it should not be served from the booking database.
- Can bookings be cancelled or changed? Each cancellation must give the room back exactly once, which makes release logic as important as reserve logic.
Requirements
Functional
- Search hotels by place, dates and guests, with the availability and price for each.
- Book a room type for a range of nights, with payment.
- Hold a room for a short time while the guest pays.
- Cancel a booking and give the room back to the pool.
Non-functional
- No double booking: reserved never exceeds the sellable count for any room type and night.
- A repeated booking request creates one booking, not two.
- Search: p99 under 300 ms, with availability no more than a few seconds old.
- Handles 11,600 searches and 120 booking attempts a second at peak.
Back-of-the-envelope estimates
Assumptions for this exercise:
- These numbers are assumptions for the exercise, not facts about any real platform.
- 100,000 hotels with about 10 room types each, so 1 million room types. We sell up to 365 nights ahead, and a row takes about 50 bytes.
- 1 million bookings a day, and 100 searches for each booking. Peak traffic is 10 times the average.
- A typical stay is 3 nights.
The arithmetic:
- Bookings per second: 1M a day ÷ 86,400 s ≈ 11.6 → ≈ 12/s on average, about 120/s at a 10x peak
- Searches per second: 100 × 1M = 100M a day ÷ 86,400 s ≈ 1,157 → ≈ 1,160/s on average, about 11,600/s at a 10x peak
- Inventory rows: 1M room types × 365 nights → 365 million rows
- Inventory size: 365M × 50 bytes → ≈ 18 GB, small enough for one database with replicas
- Rows locked per booking: 3 nights, one row per night → 3 rows in one transaction
High-level design
A Client reaches an API gateway, which sends searches to the Search service and bookings to the Booking service. They are separate because they have opposite needs: search wants speed and can be a few seconds stale, while booking wants exact answers.
The Search service reads from the Search index, a copy of hotel data with cached availability and prices. It is kept current by change events that flow from the Inventory DB, the source of truth.
The Booking service works against the Inventory DB, which holds the number of rooms sold for each room type and night. It reserves the nights in one transaction, then charges the guest through the Payment provider, then confirms. The hold expires if payment never completes.
How a request flows
Search for rooms
- A traveller searches for a city, dates and number of guests.
- The gateway routes the search to the Search service.
- The service queries the search index for hotels that match and have a room for every night.
- The index answers from its cached counts and prices, which are at most a few seconds old.
- The service ranks the hotels and builds the result page.
- The traveller sees hotels and prices. Availability is advice, not a promise, until they book.
Book a room
- The traveller picks a room type and dates, and the app adds a unique request key.
- The gateway routes the request to the Booking service.
- In one transaction it adds a room to the reserved count of each night, only where rooms remain, and records a pending booking.
- If every night was updated, the rooms are held for 10 minutes. If any night is full, the transaction rolls back.
- The service charges the card through the payment provider, passing the same request key.
- The provider reports that the payment succeeded.
- The service marks the booking confirmed, so the hold no longer expires.
- It tells the gateway the booking is confirmed, and the traveller sees the confirmation.
Availability changes
- A booking commits and the counts for its nights change.
- The database publishes a change event, and the search index updates its cached counts within seconds.
- The next search that reads this hotel sees the new count, including a sold-out night.
Deep dive: inventory as counts per room type and night
Do not model each physical room. Guests book a room type, such as 'double, city view', and the hotel assigns the actual room at check-in. So availability is just a number for each room type and each night.
One table row holds one room type on one night, with its key as the room type and the date. Its columns are the number of rooms sellable and the number already reserved. Available rooms is sellable minus reserved. In our estimate that is 365 million rows of about 50 bytes, so around 18 GB. A single relational database with replicas holds that comfortably.
A stay is a range of nights, not days. Checking in on the 1st and out on the 3rd occupies the nights of the 1st and 2nd, and the room is free again on the evening of the 3rd. Store dates in the hotel's own time zone, to avoid errors around midnight.
Booking a stay means updating one row per night, all together or not at all. That is one database transaction. A 3-night stay locks 3 rows, which is tiny.
Keep a separate column for the sellable count. Normally it equals the rooms owned. If the business chooses to overbook, it is set a little above. A check constraint, reserved not above sellable, makes the database refuse anything past the limit, whatever the application code does.
Shard by hotel. All the rows for one hotel live together, and a booking is always for one hotel, so no transaction ever spans two shards.
Deep dive: no double booking: a conditional decrement
The danger is a race. Both guests read 'one room left', both continue, and both get a confirmation. A check in the application followed by a write is not safe.
The fix is to make the check and the change a single atomic statement, in one transaction:
- Update the rows for the requested room type and nights, adding one to the reserved count, but only for rows where reserved is still below sellable.
- Compare the number of rows changed with the number of nights. If it matches, every night had space, so commit. If not, at least one night was full, so roll back.
The database serialises concurrent updates on the same row, so exactly one of the two guests wins the last room. Update the nights in date order in every transaction, so two bookings that overlap lock rows in the same order and cannot deadlock. Pessimistic locking with a row lock, or an optimistic version column with a retry, also work. Avoid distributed locks. The database already has what you need.
Idempotency. Mobile networks time out, and people double-click. The client sends a unique request key, and a unique constraint on that key makes a second attempt return the first booking instead of creating another.
The hold. A booking is a small state machine: pending (the hold, with an expiry time such as 10 minutes), confirmed after payment, or cancelled and expired. A background job finds pending bookings past their expiry, moves them to expired and subtracts one from the reserved counts. That change should happen only when the state change itself succeeded, so a release can never run twice. Payment is charged with the same request key, so a retry cannot charge the card twice. If payment times out with an unknown result, ask the provider what happened before releasing the hold.
Deep dive: search, rechecking and overbooking
Searches are about 100 times as common as bookings. Serve them from a separate search index that holds hotel details, location, prices and per-night availability counts. It supports filters and ranking, and it can be scaled out with replicas without touching the booking database.
The index is fed by change events from the inventory database: each committed booking or cancellation produces an event that updates the cached count. The index is therefore a few seconds behind, which is fine for browsing.
Because of that lag, the search answer is advice. Booking must re-check against the source of truth, which the conditional update does automatically. When a guest tries to book a room that has just sold out, say so plainly and offer the nearest alternatives. Re-check the price in the same step too. If it changed, show the new price before taking payment.
Be careful with urgency messages such as 'only 2 left'. Show them only when the count is fresh, or round them off, so you never promise what you cannot give.
Overbooking is a business choice. Hotels know that some guests do not turn up, so they may sell slightly more than they own. In the model it is only a higher sellable number. The system does not decide the percentage. It must support the follow-through, though: a way to find an overbooked night and a process to move a guest to a comparable hotel.
Cancellations are the mirror image. Move the booking to cancelled, subtract one from each night's reserved count in the same transaction, and publish the change so the search index sees the room again. Make the release idempotent, as with the hold.
Trade-offs
- Booking consistency: A strongly consistent relational database over An eventually consistent store. A conditional update makes double booking impossible. The cost is that writes for one hotel go through a single primary.
- Search data: A separate index fed by change events over Query the inventory database directly. Search load never slows bookings and can scale on its own. The cost is a few seconds of staleness and a pipeline to keep in sync.
- Payment and rooms: Hold the rooms, then pay, then confirm over Charge first, then try to reserve. A guest never pays for a room that is gone. The cost is a hold that can expire and a job to release it.
- Sharding: By hotel over By guest. Every booking touches one shard and transactions stay local. The cost is that one very busy hotel cannot be spread across shards.
Common follow-up questions
What if the payment succeeds but your service crashes before confirming?
The booking stays pending, and the payment provider sends a webhook about the successful charge. A handler matches it by the request key and confirms the booking. A periodic job also compares pending bookings with the provider, and releases the room only if there was no payment.
How would you handle a hotel with thousands of rooms on a peak night?
The counts stay small, so the design does not change. The only pressure is many bookings updating the same few rows, which the database handles one at a time. At about 120 bookings a second over the whole platform, even a very busy hotel sees only a few a second.
How would you let guests choose a specific room?
Add a room table and treat each room's stay as an interval that must not overlap another, enforced by an exclusion constraint or by locking the room's rows. It is more complex than counts, so many systems assign rooms at check-in instead.
How do you deal with hotels that sell through other channels as well?
Treat each partner as a source of inventory changes. Push every booking made here to them, and take their changes by an API or a feed, with the same conditional update. Keep a little buffer of rooms unsold when the link is slow.