The system design question
Design a hotel reservation system with users and admins. Users book rooms for a date range, and admins manage rooms, prices and see their bookings.
Agree on the scope (5 minutes)
Say up front what you're covering.
| We'll design | We'll skip |
|---|---|
| Guests booking a room for a date range | Search and availability browsing |
| Cancelling a booking | How payment is processed inside |
| Admins managing rooms and prices | Reviews and messaging |
| Admins seeing their bookings | Loyalty points and discounts |
Then agree on what "good" means:
- Never double-book: two guests can't get the same room for the same night, ever.
- Safe to retry: if a guest's phone sends 'book' twice, they get one booking.
- Fair prices: a guest pays the price they saw, even if the admin changes it a second later.
- Admins see changes quickly: a new booking shows up in the hotel's view within seconds.
Rough numbers (5 minutes)
These are assumptions, made up but reasonable: 1 million properties, 1.5 million bookings a day, peaks at 10 times the average, about 1 KB per booking, and bookings kept for years.
- ~17
- bookings per second, on average
- ~170
- bookings per second at a peak
- ~550 GB
- of bookings a year
- ~7B
- room type and night rows
| Question | Rough math | What it means |
|---|---|---|
| How many writes? | 1.5 million bookings ÷ 86,400 seconds × 10 | About 170 a second at a peak, which a well-split relational database handles fine |
| How much storage? | 1.5 million × 1 KB × 365 days | About 550 GB a year, so we'll split by hotel eventually |
| How many rows does a booking touch? | One row per night of the stay | Usually 1 to 7 rows, all for the same hotel |
The overall design (10 minutes)
- A guest picks a room type and dates. The app sends a booking request with a unique request ID.
- The booking service, in one database transaction, takes one room off the count for every night of the stay. If any night is sold out, nothing changes.
- The booking starts as 'held' for 10 minutes while the guest pays. If payment goes through it becomes 'confirmed'. If not, the rooms go back.
- Each change publishes an event, which updates the hotel's booking list, sends confirmation emails, and tells search the room count changed.
- Admins edit room counts and prices through the same service, so the same checks apply to them.
What the apps need to do
| Action | Who does it | What's sent |
|---|---|---|
| Create a booking | Guest | Request ID, hotel, room type, check-in, check-out, guests, quoted price |
| Cancel a booking | Guest or admin | Booking ID |
| Set rooms or prices | Hotel admin | Hotel, room type, nights, room count or price |
| List bookings | Hotel admin | Hotel, date range, status, page |
What we store and where
| Data | Where it goes | Why |
|---|---|---|
| Rooms per room type per night (total and booked) | Relational database, split by hotel | Transactions keep counts correct when two people book at once |
| Prices per room type per night | Same database, same split | So a booking reads prices and rooms together |
| Bookings | Same database, same split | One transaction covers the booking and the room counts |
| Admin booking list | A copy built from events | Lets admins filter and page without slowing down bookings |
Step 1: Never double-booking (12 minutes)
First decide what a guest books. Hotels mostly sell a room type, like 'double with a view', and assign the actual room number at check-in. So we track, for each room type and night, how many exist and how many are booked.
Booking a 3-night stay means bumping 'booked' by one on 3 rows, but only if each one is still under the total. Do it in one transaction, with each update saying 'add one only if booked is less than total'. If any of the 3 updates changes nothing, that night is full, so roll the whole thing back. The database makes the check and the change happen as one step, so two guests can't both grab the last room.
This leans on the database's guarantees, which is why we picked a relational one. It's a case where we want strong consistency over speed, and our numbers say we can afford it.
What about locking the rows first?
Locking each night's row, then checking, then updating also works. It's more steps and you have to lock rows in the same order every time, like by date, or two bookings can wait on each other forever. The 'update only if there's room' version does the same job with less to get wrong.
Step 2: Retries and payment (10 minutes)
Phones lose signal and people double-tap. If the app retries 'book', we don't want two bookings. So the app makes a unique request ID when the guest taps 'book' and sends it every time it retries. The booking table has a unique rule on that ID, so a second attempt finds the first booking and returns it. That's idempotency.
Payment can take a while and can fail. So a booking starts as 'held' with an expiry time. The rooms are taken off the count, so nobody else can grab them while the guest pays. If payment succeeds, mark it confirmed. If it fails or the hold expires, a cleanup job cancels it and puts the rooms back.
Pass the same request ID to the payment provider so a retried payment doesn't charge the card twice.
Step 3: Admins changing rooms and prices (8 minutes)
An admin might lower the number of double rooms from 10 to 6 when 8 are already booked. Don't allow that silently. Reject it, or let them lower it only to what's booked and show them which bookings they'd need to move.
Prices change all the time. The booking saves the total price at the moment it was made, so a later price change never touches existing bookings. If the price changed between the guest seeing it and tapping 'book', compare against the quoted price they sent and ask them to confirm the new one.
Admins need to see bookings, but listing and filtering shouldn't compete with guests booking. Build their list from the event stream into a separate copy they can query. It's a few seconds behind, which is fine for that screen. Only staff of that hotel can see its bookings, which comes down to checking who's logged in and what hotels they belong to.
Choices you should be able to defend
| We chose | Instead of | Because | We'd reconsider if |
|---|---|---|---|
| A relational database with transactions | A database without them | Our traffic is small and correctness matters most | Writes grew far past what one split can handle, and we could accept more complexity |
| Booking room types, not room numbers | Booking specific rooms | It's how hotels actually sell, and it's far fewer rows to check | A property sells unique units, like apartments |
| Holding rooms while the guest pays | Taking rooms only after payment | The guest never pays for a room that's then gone | Too many holds expired and blocked real bookings |
| Splitting by hotel | Splitting by guest | A booking only ever touches one hotel, so the transaction stays on one machine | Most lookups were by guest, which a separate copy can handle |
What could go wrong (5 minutes)
| What happens | What we do about it |
|---|---|
| Two guests try for the last room at once | One update wins, the other sees zero rows changed and gets 'sold out' |
| The app retries a booking | The request ID matches the first one, so we return that booking |
| Payment never comes back | The hold expires after 10 minutes and the rooms go back on sale |
| The event stream falls behind | Bookings are still correct. Admin lists and emails just arrive late |
| A database split goes down | Bookings for those hotels fail until a standby copy takes over. Others keep working |
Follow-up questions (5 minutes)
How would you support overbooking, like airlines do?
Let each room type have an allowed total above the real number, like 105% for 100 rooms. The same 'add one only if under the limit' check still works.
What if a guest wants two rooms?
Same transaction, more rows. Either both room types have space on every night or nothing changes.
How do bookings from other sites fit in?
Hotels often sell the same rooms elsewhere. Those come in through an API that does the same conditional update, and the hotel's system and ours keep counts in sync with events.
How the 60 minutes add up
| Minutes | What you're doing |
|---|---|
| 0–5 | Agree on what you're building |
| 5–10 | Rough numbers |
| 10–20 | Draw the overall design |
| 20–32 | Never double-booking |
| 32–42 | Retries and payment |
| 42–50 | Admins changing rooms and prices |
| 50–55 | What could go wrong |
| 55–60 | Follow-up questions |
Key concepts used here
- Consistency and the CAP theoremWhat happens when copies of your data can't agree.
- Idempotency and retriesMake sure a retried request doesn't happen twice.
- Relational vs non-relational databasesPick a database based on how you'll actually use it.
- Sharding and consistent hashingSplit data across machines and keep it balanced.
- Authentication and access controlWork out who someone is and what they're allowed to do.
Further reading
We wrote this sample answer ourselves.
Practice Loop is independent and isn't affiliated with or endorsed by Booking.com.