Appearance
9.7.13 — The Booking Family: Hotel, Car Rental, Airline, Concert
Four prompts that interviewers rotate between:
"Design a hotel booking system." "Design a car rental system." "Design an airline reservation system." "Design a concert ticketing system."
They are the same problem. A limited set of things, each claimable for a period of time, with a price that varies and a claim that two people can race for. Once you can see the shape, all four are the same twenty minutes of work with different nouns — and the useful skill is spotting exactly where each one differs, because that is where the interesting design is.
This page builds the shared skeleton once, then works the four differences properly.
1. The shared skeleton
Every one of these is allocation over a time window. The skeleton is always:
| Piece | What it is |
|---|---|
| Resource | The thing being claimed — room, car, seat |
| Inventory unit | The resource for one time period |
| Claim | A conditional write that can fail |
| Hold | A temporary claim with an expiry |
| Booking | The lifecycle: held → paid → confirmed → used |
| Pricing | A strategy, because it always varies |
The single most important modelling rule, and it is the same one as 9.7.9: availability belongs to the pair of (resource, time period), never to the resource. A room is not "booked"; a room is booked for the nights of the 4th and 5th. Putting isBooked on the room makes the second night unrepresentable, and it is the mistake that ends the interview early.
Everything else follows from what "time period" means, and that is the axis the four problems differ on.
2. Where the four differ
Question one: is the customer claiming a specific unit or any unit of a type?
If any unit will do, you do not need to track individual units at all — a count per type per period is enough, and the claim becomes a conditional decrement. That is far simpler and far faster, and it is why hotels do not assign you a room number until you arrive.
If a specific unit matters — seat 14A, that particular car — you need a row per unit per period, and the claim is a conditional update on that row.
Question two: is the period a range the customer picks, or a fixed slot?
A fixed slot (a flight, a concert) means one inventory record per unit per event. Simple.
A range (three nights, five days) means a booking spans several periods and every one of them must be available or none of it works. That is a different and harder claim, and section 4 is about it.
3. Hotel: counts, not rooms
The hotel is the "any unit, range of nights" corner, and the design that follows is much lighter than people expect.
typescript
interface RoomTypeInventory { // (1) one row per type per night
hotelId: HotelId;
roomType: RoomType; // double, twin, suite
date: LocalDate; // (2) one night
total: number;
booked: number; // (3) a count, not a set of rooms
}(1) The unit of inventory is a type on a night, not a room.
(2) One row per night, so a three-night stay touches three rows.
(3) Just a number. You are not tracking which rooms are taken, because it does not matter until check-in — and deferring that decision is what lets a hotel absorb a broken shower or a guest extending a stay without cancelling anybody.
The claim for a three-night stay must be all-or-nothing:
sql
UPDATE room_type_inventory
SET booked = booked + 1
WHERE hotel_id = :hotel AND room_type = :type
AND date IN ('2026-08-04', '2026-08-05', '2026-08-06')
AND booked < total;Run this inside a transaction and require that it affects exactly three rows. If it affects two, one night was full, and you roll back. A booking that gets two of three nights is worse than no booking at all, because the guest arrives and finds themselves homeless on the Wednesday.
Two consequences worth stating.
Room assignment is a separate, later concern. At check-in, pick any free room of that type. That is a small allocation problem with none of the concurrency, and separating it is why hotels can oversell one type and upgrade a guest into another.
Range queries are the expensive read. "Show availability for August" means scanning thirty-one rows per type. That is cheap. "Find every hotel in Paris with a free double for these three nights" is the query that needs thought, and the standard answer is a per-hotel-per-date summary table maintained on write, because computing it on read across ten thousand hotels is not viable.
4. Car rental: the unit moves, and that changes everything
Cars are specific units, so you need a row per car per period. But cars have a property no other member of this family has: they are somewhere, and after a rental they are somewhere else.
typescript
interface Rental {
carId: CarId;
from: Instant;
to: Instant;
pickupBranch: BranchId;
dropoffBranch: BranchId; // (1) may differ from pickup
}(1) This one field is the whole difference. A one-way rental leaves the car at the destination, so its next availability is at a different branch.
Three problems follow, and naming them unprompted is what makes this answer good.
Availability is location-dependent and time-dependent together. A car free next Tuesday is only useful to a customer collecting from the branch where it will actually be. So the availability check is not "is this car free" but "is this car free and will it be at this branch by then", which means the answer depends on every booking before it.
Fleet imbalance is a real operational cost. One-way rentals drain cars from one city and pile them up in another. Real systems either price one-way rentals to discourage the unpopular direction, or plan repositioning — moving cars between branches — as a scheduled activity that occupies the car and must therefore appear in the availability calendar exactly like a rental does. Modelling a repositioning as a booking with no customer is the neat move here.
Buffer time between rentals is mandatory. A car returned at 10:00 cannot be collected at 10:00; it needs cleaning, checking and refuelling. So an interval is not "from 10:00 to 14:00" but "from 10:00 to 14:00 plus a turnaround". Forgetting this produces a system that is correct in the database and impossible in the car park, and it is a detail interviewers like because it can only come from thinking about the physical thing.
The overlap check is the core query, and it is worth writing out because people get the boundary conditions wrong:
sql
SELECT 1 FROM rentals
WHERE car_id = :car
AND from_time < :requestedTo + :turnaround -- (1)
AND to_time + :turnaround > :requestedFrom; -- (2)(1) and (2) are the standard two-sided overlap test. Two intervals overlap when each starts before the other ends. Getting this wrong in one direction produces double-bookings; getting it wrong in the other refuses valid bookings, which is invisible because nobody complains about a booking they were never offered.
5. Airline: specific seats, and deliberate overbooking
Seats are specific and the period is a fixed flight, so the inventory is one row per seat per flight — the same shape as 9.7.9's show-seats, and the same conditional claim.
Two things make the airline version genuinely different.
Fare classes are not seat types. A flight sells a limited number of seats at each price, and those buckets are not physical categories — the same economy seat can be sold as a cheap advance fare or an expensive flexible one. So inventory has two dimensions: physical seats, and fare buckets with counts. A booking consumes one of each, and a common modelling error is collapsing them into one.
Overbooking is deliberate policy, not a bug. Airlines sell more seats than exist, because a predictable fraction of passengers do not show up. That means the "sold" count may legitimately exceed capacity, and the design must permit it:
typescript
interface OverbookingPolicy {
maxSold(flight: Flight): number; // capacity × a factor from historical no-show rates
}And it means there must be a denied-boarding process — compensation, rebooking, a bidding system for volunteers — which is business logic that has to exist somewhere. An interviewer asking "what if everyone turns up?" is checking whether you treat that as an error or as a planned path. It is a planned path.
Seat assignment is also separable, like the hotel's room assignment. Many passengers do not choose a seat, so the system holds a fare booking and assigns a physical seat later, at check-in. That separation is what lets an airline swap the aircraft for a different model and reassign everybody, which happens constantly.
6. Concert: no time dimension, one enormous burst
A concert is the simplest data model in the family and the hardest operational problem.
There is exactly one event, so there is no time range and no calendar. For reserved seating it is one row per seat, identical to the cinema. For general admission it is a single counter, and the claim is one conditional decrement.
All the difficulty is in the burst. Fifty thousand people arrive in the same second when tickets go on sale, and they all want the same small set of rows.
The correctness part is already solved by the conditional write — it cannot oversell no matter how many people arrive, at any number of servers. Say that clearly first, because it separates the two problems.
What remains is capacity and fairness, and those need different tools.
A waiting room. Admit users to the purchase flow at a controlled rate and show everybody else a queue position. This converts a stampede into a line, keeps the database receiving load it can handle, and — the part that matters commercially — gives every customer a definite answer rather than fifty thousand simultaneous failures.
A hold with a short expiry. Ten minutes is generous for a cinema and far too long here, because a held-and-abandoned seat during an on-sale is a lost sale at the exact moment demand is highest. Two or three minutes, and it is a business parameter, not a constant.
Per-customer limits enforced at claim time. "Four tickets per person" has to be checked in the same statement that claims them, or a script buys four hundred.
The full capacity funnel for this is Chapter 11.16; the LLD answer is the counter, the hold, and the limit, plus naming the queue as the thing that sits in front.
7. What they all share: the booking lifecycle
Whichever of the four you are asked, the booking itself is the same state machine:
typescript
type BookingState =
| { kind: "held"; until: Instant }
| { kind: "confirmed"; paymentRef: PaymentRef }
| { kind: "checkedIn" } // used, in progress
| { kind: "completed" }
| { kind: "cancelled"; refund: Money }
| { kind: "noShow" }; // (1)(1) The state everybody forgets, and every one of these businesses has it. A no-show is not a cancellation: the customer is charged differently, the inventory is released differently, and in the airline case it is the whole reason overbooking works. Including it unprompted shows you thought about the business rather than the happy path.
Three rules that apply to all four.
The hold expires by being ignored, not by being deleted. Every availability query treats a hold whose expiry has passed as free. No background job writes to inventory, so no sweeper can race a customer confirming at that exact moment. A cleanup job is housekeeping with no correctness role (9.7.9).
Cancellation policy is a strategy. Refund rules differ by product, by how close to the date you cancel, and by fare type. It varies, so it is an interface.
Confirmation is conditional on the hold still being valid. Payment can succeed after the hold lapsed, and the answer is to make the confirming write conditional and refund automatically when it affects zero rows.
8. What the interviewer will push on
"Where does availability live?" The question all four turn on. Availability belongs to the (resource, period) pair, never to the resource. If you put isBooked on the room, the second night is unrepresentable and the design is finished.
"Do you track individual rooms?" They are testing whether you noticed the interchangeability question. For a hotel, no — a count per type per night is enough, and room assignment is a separate, later, much simpler problem. For a seat or a specific car, yes. Getting this right makes the hotel design dramatically lighter than candidates who model every room.
"A three-night booking where the middle night is full." All-or-nothing in one transaction, affecting exactly three rows or rolling back. A partial booking is worse than a refusal, because the guest finds out on the Wednesday.
"What is different about car rental?" The car moves. A one-way rental means the next availability is at another branch, so availability is location-dependent as well as time-dependent. Then the two details that only come from thinking about the physical object: turnaround time between rentals, and repositioning modelled as a booking with no customer.
"An airline sells more seats than it has. Is that a bug?" No, it is policy driven by historical no-show rates, and it requires a denied-boarding process as a designed path rather than an error. Add that fare buckets and physical seats are two separate inventories, and that a booking consumes one of each.
"Fifty thousand people at 9am for a concert." Separate correctness from capacity. Correctness is already handled by the conditional claim and cannot oversell. Capacity is a waiting room admitting users at a controlled rate, a much shorter hold than a cinema uses, and a per-customer limit enforced in the claiming statement.
The thing to volunteer that nobody asks for: the noShow state. Every one of these businesses has customers who simply do not turn up, and it is a different state from cancellation with different money, different inventory release and — for the airline — the entire justification for overbooking. Candidates model the happy path and the cancel path; adding the third one signals you have thought about the business being built rather than the exercise being set.
Recall
- All four are allocation over a time window. Availability belongs to the (resource, period) pair, never to the resource.
- Two questions place any booking prompt: is the unit interchangeable or specific, and is the period a customer-chosen range or a fixed event?
- Hotel — interchangeable, so inventory is a count per type per night, not a set of rooms. Room assignment is a separate, later problem. A multi-night claim must affect exactly N rows or roll back.
- Car rental — the unit moves. One-way rentals make availability location-dependent, create fleet imbalance, and need turnaround time between rentals. Model repositioning as a booking with no customer.
- Airline — fare buckets and physical seats are two inventories; a booking consumes one of each. Overbooking is deliberate policy and requires a denied-boarding path. Seat assignment is separable, which is why aircraft swaps are possible.
- Concert — trivial data model, enormous burst. Correctness is already handled by the conditional claim; what remains is a waiting room, a much shorter hold, and a per-customer limit enforced in the claiming statement.
- Shared lifecycle: held → confirmed → checkedIn → completed, plus cancelled and the one people forget, noShow.
- Holds expire by being ignored, cancellation rules are a strategy, and confirmation is conditional on the hold still being valid.
Self-test: What two questions place a booking prompt on the chart? Why does a hotel not track individual rooms? What must a three-night claim do if one night is full? Name three things that make car rental different. Why is overbooking not a bug? Which booking state do candidates always forget?
Quiz Bank
FoundationalShow that hotel booking, car rental, airline seats and concert tickets are the same problem, and say precisely where each one diverges.
The shared shape is allocation over a time window. In every case there is a limited set of things, each claimable for some period, with a price that varies, a claim two customers can race for, and a booking that moves through a lifecycle. Build that once and all four are the same twenty minutes.
The one modelling rule that governs all of them: availability is a property of the pair (resource, period), never of the resource. A room is not booked; a room is booked for these nights. Anything that puts a boolean on the resource cannot represent the second night, and it is the mistake that ends the interview early.
Two questions then separate the four.
Is the customer claiming a specific unit, or any unit of a type? If any will do, you do not need to track units at all — a count per type per period is sufficient and the claim is a conditional decrement. If a specific unit matters, you need a row per unit per period.
Is the period a range the customer chooses, or a fixed event? A range means a booking spans several periods and all of them must succeed together. A fixed event means one record per unit.
Where each one diverges.
Hotel — interchangeable units, chosen range. So: counts per room type per night, an all-or-nothing multi-night claim, and room assignment deferred to check-in, which is what allows upgrades and absorbs a broken shower without cancelling anybody.
Car rental — specific units, chosen range, and the unique property that the unit moves. A one-way rental changes where the car will be, so availability depends on location as well as time. It also needs turnaround time between rentals and a way to model repositioning, which is best done as a booking with no customer attached.
Airline — specific units, fixed event, plus two things nothing else has: fare buckets as a second inventory alongside physical seats, and deliberate overbooking with a denied-boarding process as a designed path.
Concert — the simplest model and the hardest operations. Often a single counter, no time dimension at all, and fifty thousand simultaneous claims. Correctness is free from the conditional write; the work is a waiting room, a short hold, and per-customer limits.
The reason this framing is worth having is that it turns an unseen prompt into a two-question classification. "Design a meeting room booking system" is interchangeable-or-specific (specific, people care which room) and range-or-fixed (range, chosen slots) — so it is the car rental shape without the movement, and you already know the model.
AppliedDesign the availability check for a car rental where customers can pick up in one city and drop off in another.
Start by naming why this is harder than a hotel. A hotel room is where it is. A car that is rented one-way ends the rental somewhere else, so "is this car available on Tuesday" has no answer without also asking "and where will it be?"
So availability is a function of the car's whole future schedule, not of a single row. The car's bookings form a timeline, and each one both occupies a period and determines the car's location at the end of it.
The core query is the overlap test, and the boundary conditions are where people go wrong:
sql
SELECT 1 FROM rentals
WHERE car_id = :car
AND from_time < :requestedTo + :turnaround
AND to_time + :turnaround > :requestedFrom;Two intervals overlap exactly when each starts before the other ends. Getting the comparison wrong in one direction allows double-bookings; getting it wrong in the other silently refuses valid bookings, which nobody reports because customers do not complain about an option they were never shown.
Turnaround time is not optional. A car returned at 10:00 cannot be collected at 10:00 — it needs cleaning, inspection and fuel. So every interval is effectively extended by a turnaround buffer on both sides, which is why it appears in the query rather than being handled in the application. This detail only comes from thinking about the physical object, which is why interviewers like it.
Then the location constraint, which is the part unique to this problem. For a car to be collectable at branch B on Tuesday, the booking immediately preceding Tuesday must end at branch B. So the check becomes: find the last rental ending before the requested start, and require its drop-off branch to equal the requested pick-up branch. A car sitting idle at a branch has its location from its last completed rental, or from where it was last repositioned.
Repositioning is the neat modelling move. Fleet managers move cars between branches to correct imbalance. That movement occupies the car and changes its location, which is exactly what a rental does. So model it as a booking with no customer. Now the availability query needs no special case at all, and the calendar is complete.
Two extras worth volunteering.
One-way rentals need pricing that reflects their operational cost, because a popular one-way direction drains a city of cars. Charging more for the unpopular direction — or less, to encourage rebalancing — is a pricing strategy, which is another reason pricing is an interface.
The search query is the expensive one. "Any car of this class available at this branch for these dates" across a fleet of thousands is not something you want to compute from raw rentals on every search. Maintain a per-branch, per-class, per-day availability summary updated on write, and treat it as a cache whose truth is the rentals table — the same relationship as the cached balance and the ledger in 9.7.10.
InterviewAn airline sells 320 tickets for a 300-seat aircraft. Explain why this is not a bug, and what the design must include.
It is deliberate, and it is driven by data. A predictable fraction of passengers do not board — missed connections, changed plans, illness. Historically that is somewhere between five and fifteen percent depending on route, time of day, and fare type. Flying with thirty empty seats on a full-price flight is a large, permanent revenue loss, so airlines sell above capacity by a factor derived from the observed no-show rate for that specific route and season.
So the design must permit sold to exceed capacity, which means the inventory check is not sold < capacity but sold < maxSold(flight), where the limit comes from a policy object rather than from the aircraft. That policy is a strategy: it varies by route, by season, by fare mix, and it is adjusted by revenue management people rather than by engineers.
And it must include a denied-boarding process, because sometimes everybody turns up. This is business logic that has to live somewhere, and it has a defined order in most jurisdictions: ask for volunteers first, with escalating compensation, then deny boarding involuntarily by a documented rule, then rebook and compensate according to regulation. Treating "more passengers than seats" as an error condition rather than as a designed path is the failure this question is looking for.
Two further design points that come with it.
Fare buckets are a separate inventory from physical seats. A flight sells a limited number of seats at each price point, and those buckets are not physical categories — the same economy seat is a cheap advance fare on Monday and an expensive flexible fare on Friday. A booking therefore consumes one seat and one unit of a fare bucket, and collapsing the two into a single count is a common modelling error that makes revenue management impossible to express.
Seat assignment is separable from booking. Many passengers never choose a seat, so the system holds a fare booking and assigns a physical seat later, often at check-in. That separation is what allows an aircraft swap — replacing a 300-seat plane with a 280-seat one — to be handled by reassigning seats rather than by cancelling bookings. If seats were assigned at purchase and treated as the booking's identity, every equipment change would be an incident.
The general lesson worth stating, because it transfers beyond airlines: an inventory limit is a business parameter, not a physical fact. Restaurants overbook tables, hotels overbook rooms, and cloud providers oversubscribe hardware, all for the same reason and all with a defined process for the case where the bet loses. Modelling the limit as policy rather than as capacity is what makes that expressible.
StaffDesign the search side of a hotel platform: find every hotel in a city with a room available for three specific nights, sorted by price. Ten thousand hotels, a hundred million bookings.
Say first that this is a different problem from booking, and that the split is the design. Booking is a write with a strict correctness requirement and low volume. Search is a read with a soft correctness requirement and enormous volume — perhaps a thousand searches for every booking. Serving both from the same structures optimised the same way gets you a system that is slow at search and risky at booking.
Why the obvious query does not work. For each of ten thousand hotels, for each room type, check three nights, then join prices and sort. That is millions of row reads per search, and the sort cannot start until it is all done. At a thousand searches per second it is hopeless.
The core move: precompute a per-hotel, per-date availability summary. One row per hotel, per room type, per date, holding total, booked and the lowest price for that night. It is maintained on every booking and cancellation, in the same transaction, so it is exactly correct rather than eventually correct — and it turns "is this hotel available" from a scan of bookings into three row reads.
That alone reduces the work by orders of magnitude, and it is the same cached-derived-value pattern as the wallet's balance: the bookings remain the truth, the summary is derived and rebuildable.
Then the second move: a search index rather than the database. A document per hotel holding its location, attributes, and a compact availability structure — for instance a bitmap over the next 365 days per room type, where a bit means "at least one room free". A three-night query becomes a bitwise operation, and the filter by city and attributes is what search engines are built for. Rebuild or update documents from the summary table as bookings land.
Accept staleness deliberately, and say so. A search result that is a few seconds out of date is fine, because the claim at booking time is conditional and will reject a room that has since gone. The user experience for that case — "this just sold out, here are similar options" — has to be designed, and it is far cheaper than making search perfectly consistent. Search is a hint; the claim is the truth, and that separation is what makes the whole thing tractable.
Pricing is the part that is easy to underestimate. Sorting by price means the price must be known for every candidate, and hotel pricing is dynamic — it depends on the dates, the length of stay, occupancy, and the specific promotion. Computing it per hotel per search is expensive. The practical answer is to precompute a nightly lowest price per hotel per date into the same summary, use it for filtering and coarse sorting, and compute the exact total only for the page of results actually being displayed. Sorting by an approximate price and then correcting the visible page is a compromise worth naming explicitly, because the alternative is either a slow search or a wrong sort.
What I would monitor, since this is the part that decays silently: the divergence between the summary and the bookings table, checked by a periodic recount; the rate of claim failures at booking time, which is the direct measure of how stale search has become; and search latency percentiles, since the tail is what users feel.
And the failure mode to design for from the start. If the summary is wrong in the direction of showing availability that does not exist, users see failures at the last step, which is the most expensive place to fail. If it is wrong in the direction of hiding availability, you lose bookings silently and nobody reports it. So the recount job matters more than it looks, and the alert should fire on any divergence rather than on a threshold — because a summary that drifts by one is a summary that will eventually drift by a hundred.
Flashcards
FlashThe two classifying questions
Is the unit interchangeable or specific? Is the period a chosen range or a fixed event? Hotel: interchangeable + range. Car: specific + range. Airline: specific + fixed. Concert: often just a counter.
FlashHotel inventory
A count per room type per night, not a set of rooms. Room assignment is deferred to check-in. A multi-night claim must affect exactly N rows or roll back.
FlashWhat makes car rental different
The unit moves. One-way rentals make availability location-dependent, cause fleet imbalance, and need turnaround time. Model repositioning as a booking with no customer.
FlashAirline specifics
Fare buckets and physical seats are two inventories; a booking consumes one of each. Overbooking is policy from no-show data, with denied boarding as a designed path. Seat assignment is separable, which is why aircraft swaps work.
FlashConcert on-sale
Correctness is free from the conditional claim. What remains is capacity and fairness: a waiting room admitting at a controlled rate, a two-minute hold rather than ten, and per-customer limits enforced in the claiming statement.
FlashThe forgotten state
noShow. Different money, different inventory release, and for airlines it is the entire justification for overbooking. Candidates model happy path and cancel; the third one signals business thinking.
Next: 9.7.14 — board games, where the interesting question is not the board but which of the three parts each follow-up lands in.