Design hotel reservation.
The complete senior-level answer — built from the same question definition the interview simulator probes, and scored against the same rubric it grades with.
Last updated · built from the live interview engine’s question definition and rubric
FRI · MAR 13
216 → 215
left to sell · 200 physical
SAT · MAR 14
216 → 215
left to sell · 200 physical
SUN · MAR 15
216 → 215
left to sell · 200 physical
BK-4127 · deluxe king · 3 nights
all three rows decrement together, or none do — and 216 > 200 is policy, not a bug
Two ways to read this. Night before the interview: the revision sheet plus the one-line checkpoint that closes each section. With an evening: read straight through — the instruments are optional depth, not required reading.
Before you read
The answer in four anchors
Use these as the mental checklist while you skim; the sections below unpack each one.
Contract
Oversell on purpose — bounded
The contract, before any box: reserved ≤ sellable per (room type, night), sellable > physical by policy — and every promise the system can’t keep ends in a stated walk plan, not an exception handler.Scale read
Policy, not throughput
One number to keep: ~35 bookings/sec. The scale story is search fan-out and local contention; the money story is the no-show rate. Spend the complexity budget on policy, not sharding.Core commit
N rows, one transaction
Reserve N nights → attach guarantee → confirm, with the reserve as N conditional updates in one transaction — date-ordered, rowcount-checked — against a sellable ceiling deliberately above physical. Async begins after the commit.Deep dives
The night the statistics lose
Three dives, one theme: the ledger is per-night counts, and every rule above it is data — the ceiling, the window, the walk. The book path stays one transaction; the policy layer stays a config.01 · The brief · minute zero
What you are handed
Design the reservation system for a global hotel platform — search, book, cancel, check in — across ~500K properties and ~10M rooms. Think Booking.com, or a major chain’s central reservation system.
Scale: ~3M bookings/day, ~250 searches for every booking, availability queried up to a year ahead. A booking spans multiple nights and must never half-book.
Key challenges: count-based inventory per room type per night, reserving a multi-night stay atomically, overbooking as a controlled policy — with a plan for the night it misfires — bookings that hold no money until the stay, no-shows and cancellations, and a search path that scales independently of the booking path.
Decide: do you ever sell more rooms than the hotel physically has? And who exactly gets turned away when everyone shows up?
The book path
Claim, reserve N nights, attach the guarantee, confirm — one strongly consistent spine at a tiny write rate. Everything on it is a conditional update.
The nightly ledger
One row per (room type, night): reserved vs sellable, where sellable > physical by policy. A three-night stay is three rows moving together, or not at all.
Search + the front desk
The cached availability world that may lie for seconds, and the physical building where promises meet rooms — check-in, walk-ins, the night audit.
search (cached, may lie) → pick a room type · hold N nights (no money) → guarantee → confirm → outbox → PMS · night audit → no-shows → tomorrow’s ceiling
Three zones and one loop. Draw this spine in the first two minutes — the interview is fought where the cached world meets the consistent one, and where tonight’s no-shows set next month’s ceiling. The full diagram is in §06.
What is really being asked
This is not a throughput question — 3M bookings/day is ~35 a second, and saying so is your first senior signal. The real subject is inventory that is count-shaped and time-shaped: the unit for sale is not a room but a room-night, one counter per (room type, night), so a three-night booking must move three rows atomically — the multi-row twist that single-SKU checkout never has.
The second subject is a contract turned inside out. E-commerce forbids overselling; a hotel does it on purpose, because an unsold room-night expires worthless and ~8% of confirmed guests never arrive. The winning shape encodes that policy as data — a sellable ceiling above physical capacity, a hard invariant beneath it, and a priced walk plan for the night the statistics lose. Candidates fail by arguing with the brief, or by obeying it without bounds.
02 · Requirements · minutes 0–8
Four requirements and an inverted contract
Functional
- A guest searches availability by city and date range across properties; results are allowed to be seconds stale.
- A guest books a room type — not a specific room — for a date range. Most rate plans hold the booking with a card guarantee; nothing is charged until the stay. Prepaid rates pay up front.
- A guest cancels: free inside the rate plan’s window, a fee outside it. The property marks no-shows at night audit and charges per the guarantee.
- The front desk checks guests in and out, assigns the physical room at arrival — and when overbooking misfires, walks a guest under a stated compensation policy.
Requirement two hides the question’s quiet inversion of checkout: the booking holds no money. The guarantee is a promise to charge later — so the payment flow is thin exactly where the booking lifecycle is rich.
Non-functional
The contract, inverted: for every (room type, night), reserved ≤ sellable — where sellable is deliberately more than physical. E-commerce forbids overselling; hotels budget for it. Say the inversion out loud — it’s the hook of this question.
Two consistency worlds, split on purpose. Search is fanned out, cached, and allowed to lie for seconds. The book path is strongly consistent and never lies. They must not share a read path.
Latency: search ~200ms from cache; book ~500ms p99. The book path is a database transaction plus a card-token attach — no capture in the loop for guarantee rates. Knowing which leg you’re timing is the signal.
Time is property-local. A “night”, a cancellation window, a no-show cutoff — all evaluated in the property’s time zone, never UTC. The night audit is a business-day boundary, not midnight in London.
Scope cuts to state explicitly
Dynamic pricing and revenue-management internals — the rate plan and the overbook rate arrive as data; the yield model that sets them is its own system. Also cut, out loud: OTA / channel-manager sync (name the two-writer problem, don’t design the protocol), loyalty and points, housekeeping and room-assignment optimization, group blocks, and payment internals — capture, refunds, and ledgers are the payment-system question; here the PSP stores a token and charges a fee.
The contract, before any box: reserved ≤ sellable per (room type, night), sellable > physical by policy — and every promise the system can’t keep ends in a stated walk plan, not an exception handler.
03 · Back-of-envelope · minutes 8–13
Five numbers — and the one that’s policy, not throughput
These are fixed engine reference figures, not a calculator. Each result follows from the arithmetic next to it.
| Quantity | Derivation | Result |
|---|---|---|
| Room-nights sold nightly | 10M rooms × ~65% occupancy | ~6.5M room-nights |
| Booking rate | ~6.5M ÷ ~2-night stays ≈ 3M bookings/day / 86,400s | ~35 TPS avg, ~100 peak |
| Search : book | ~250 searches per booking → ~750M/day | ~9K QPS, cache-served |
| The hot night | stadium show announced · ~50K fans · ~200 rooms nearby | ~500 attempts/s, a few rows |
| Nightly ledger size | 500K properties × ~5 room types × 365 nights · ~100B/row | ~900M rows ≈ ~90 GB |
What the numbers mean — the conclusions, not the arithmetic
- ~35 bookings/sec fits one well-run Postgres with years of headroom — and at ~90 GB, so does the entire nightly ledger. Declining to shard is a decision, and it’s the decision that keeps a three-night reserve one ACID transaction.
- Search outnumbers book ~250:1 and fans out on top: one city × date-range query touches hundreds of properties × nights. That asymmetry — not booking TPS — is why the availability cache exists, and why it’s allowed to lie.
- Contention is local, not global: a hot night is one property and a handful of (room type, night) rows. Aggregate throughput stays boring while five rows catch fire — design for the rows, not the fleet.
- The interesting arithmetic is policy math: ~8% no-shows on a 200-room sellout is ~16 rooms sitting empty every “full” night. The overbook ceiling is the estimate that makes money; the TPS estimate just keeps you from overbuilding.
One number to keep: ~35 bookings/sec. The scale story is search fan-out and local contention; the money story is the no-show rate. Spend the complexity budget on policy, not sharding.
04 · Core entities · minutes 13–17
Four entities and one state machine
RoomNightInventory
property_id, room_type, night, physical, overbook_rate, reserved — one row per (room type, night), with sellable = physical × (1 + overbook_rate), derived. No table of individual rooms anywhere on the booking path: inventory is a count, and the room number is assigned at check-in.
Booking
booking_id, guest_id, property_id, room_type, check_in, check_out, rate_plan_id, idempotency_key (unique), guarantee (card token or prepay reference), expires_at while PENDING, and the state machine below. One booking, N room-nights — and while it’s PENDING, the booking row is the hold.
RatePlan
rate_plan_id, property_id, room_type, nightly price, guarantee type (card-guarantee / prepaid), cancellation window, no-show fee. The money rules are rows, not code — the booking path reads them; revenue management writes them.
Property
property_id, time zone, check-in cutoff, walk partners. The time zone is load-bearing: cancellation windows, the night audit, and “tonight” are all evaluated property-local.
The booking state machine
step with ← → or click a state
Entry — PENDING
The idempotency claim is won and all N nights are reserved in one transaction; expires_at starts ticking. No money has moved — this is a hold without a payment.
Exit
Guarantee attaches (card token or prepayment) → CONFIRMED. The clock wins → the sweeper releases the nights → CANCELLED.
What candidates get wrong here
Reserving the nights only at confirm time — the guest types card details for a stay that vanished mid-checkout. Reserve first, collect the card second.
Recurring follow-ups: “what if the guest cancels while the desk is checking them in?” (both transitions are conditional updates on the booking row — one of the two loses cleanly) and “where’s the hold table?” (PENDING is the hold: same row, an expires_at, and a sweeper).
Four entities, one shape: inventory is a count per (room type, night), the PENDING booking is the hold, the rate plan owns the money rules, and the property owns the clock.
05 · API design · minutes 17–20
Four endpoints and one header that carries the interview
GET /v1/availability?city=&check_in=&check_out=
POST /v1/bookings Idempotency-Key: <client-generated>
POST /v1/bookings/:id/guarantee
POST /v1/bookings/:id/cancel Availability is served from the cache and says so — its answer is an invitation, not a contract; the truth is asserted at book time. Creating a booking returns expires_at, so the hold is honest about its clock. Dates are property-local calendar dates, never UTC timestamps — a “night” is a business day, not 24 hours. Guarantee is its own call so the UI can hold first and collect the card second. Cancel is a POST that evaluates the rate plan’s window server-side, in the property’s time zone, and returns the fee it charged — never a DELETE.
Idempotency-Key semantics — all four cases
Pick a case: what does the server do?
New key
Reserve the nights and create the PENDING booking once; store key → booking_id (24h TTL).
Show all four cases as a table
| Case | Behavior |
|---|---|
| New key | Reserve the nights and create the PENDING booking once; store key → booking_id (24h TTL). |
| Retry: same key + same stay | Return the same booking — same hold, same expires_at; no second set of nights. |
| Same key, request still in flight | Return a conflict (409) or block briefly — never a silent second hold. |
| Same key, different stay | Reject as a validation error — the client has a bug; guessing books the wrong trip. |
Scope keys per guest and bind them to the stay (property, room type, dates) — a stale tab retrying last month’s search must not book this month’s trip.
Property-facing events — four decisions that keep the front desk and the platform honest:
- Push booking.confirmed / cancelled / no_show to the property’s PMS, signed and deduped by event id — the desk must never learn about a guest from the guest.
- Sequence events per booking — a cancel overtaking its confirm leaves the desk holding a room for nobody.
- The property writes too: walk-ins and maintenance blocks change inventory at the desk, and they flow back as adjustments through the same conditional update — one count, two doors.
- When the platform and the PMS disagree, reconcile at night audit — the property’s register is ground truth for who actually slept there.
Four endpoints, one header, and a clock in the response — expires_at keeps the hold honest, and every date stays property-local end to end.
06 · High-level design · minutes 20–30
Ten boxes around one nightly ledger
One diagram, left to right — ten boxes, deliberately. At ~35 bookings/second the win is what you decline to add. Play a three-night booking through the happy path, or click any component for what interviewers listen for there.
Every component as plain text (all inspector notes)
- Guest — external
- Searches, books, and cancels. Sees cached availability with honest staleness, a hold with a visible expires_at, and a room type — never a room number — until check-in. Treats availability as an invitation, not a contract — a clean “no longer available” at book time is correct behavior, not a bug to apologize for. Failure mode: Promising a specific room number at booking. The number is fulfillment, assigned at the desk; promising it early turns every maintenance block into a broken promise.
- API Gateway — the book path
- Authenticates, rate-limits per guest, and splits the two worlds at the front door: availability traffic goes to search and its cache; booking traffic continues inward. Saying “search and book never share a read path” while drawing this box is the cheapest senior signal on the page. Failure mode: Letting search queries reach the nightly ledger — 9K QPS of fan-out reads against the rows the book path locks is self-inflicted contention.
- Search & Availability — allowed to lag
- Answers “what’s free in Austin, March 13–15” from a precomputed cache of per-(property, room type, night) counts — refreshed by booking events, invalidated on sellout. It fans out; it never locks. Naming the staleness budget out loud — “seconds stale is fine; the book path re-asserts the truth” — is the split that carries the section. Failure mode: Making search strongly consistent. 250 reads per booking, fanned out against locked rows, melts the ledger to make a lie slightly fresher.
- Booking Service — the book path
- The orchestrator: wins the idempotency claim, reserves all N nights in one transaction, attaches the guarantee, confirms. The booking row carries the saga — any instance can pick up a crashed booking. Reserve before guarantee: nights are scarcer than card capacity, and a hold without money is cheap to release — checkout’s reserve-before-authorize, for the same reason. Failure mode: The separating probe: the guarantee attach succeeded, then the confirm update failed. Answer: the sweeper re-drives it — a PENDING hold with a guarantee on file is confirmed idempotently, never released.
- Room-Night Inventory — the nightly ledger
- One row per (room type, night): physical, overbook_rate, reserved. A reserve is one conditional UPDATE per night — reserved < sellable in the WHERE — executed in date order inside one transaction. The ceiling checked is sellable, not physical — the overbook policy is data, tuned per night without a deploy. Event nights set it to zero. Failure mode: Rowcount discipline: fewer than N rows matched means roll back. A 3-night stay that books 2 nights is corruption, not a partial success.
- Postgres — the nightly ledger
- One database holds inventory, bookings, rate plans, and the outbox — which is exactly what keeps reserve-and-confirm one ACID transaction. Partition inventory by month; past nights archive out. At ~35 bookings/sec and ~90 GB of ledger, one well-run Postgres is a feature. Say why you are NOT sharding — that’s the senior version. Failure mode: Sharding by property “for scale” hands you distributed multi-night transactions no number in this question asked for.
- PSP / Card Vault — external
- Stores the guarantee token at confirm; charges at checkout, at the no-show fee, or at the late-cancel fee. Prepaid rates capture up front. (Its insides are the payment-system question.) Payment here is thin on purpose — most bookings touch no money until the stay. The lifecycle is rich; the orchestration is two verbs and a token. Failure mode: Charging at booking on guarantee rates — every cancellation becomes a refund, and hotels live on flexible cancellation.
- Outbox → Bus — allowed to lag
- The outbox row commits with the booking; a relay publishes after commit. Consumers dedupe by event id and order per booking via sequence numbers. No event exists for a booking that never committed — the desk never preps a room for a ghost. Failure mode: Publish-then-commit mints PMS reservations for bookings that rolled back — the desk holds a room for nobody, every crash.
- Property / PMS — allowed to lag
- The hotel’s own system: room assignment at check-in, the folio, walk-ins, maintenance blocks. Learns of bookings from signed events; writes back inventory adjustments through the same conditional update. Two writers on the count — the platform and the desk — and both funnel through the conditional update; neither edits the number in place. Failure mode: Treating the PMS as read-only. Walk-ins and out-of-service rooms change sellable at the desk; ignore that and the ledger drifts from the building.
- Night Audit — allowed to lag
- The nightly jobs, property-local: expire stale PENDING holds, mark no-shows after the cutoff and charge their fees, reconcile counts against the register — and feed no-show actuals back to the overbook forecast. Closing the loop — tonight’s no-shows tune next month’s sellable — is what makes overbooking a control system instead of a gamble. Failure mode: Running the audit at UTC midnight. Property-local, or the fees, the no-shows, and “tonight” are all wrong by up to a day.
Whiteboard minimum
A passing diagram has all of these.
- Guest client (web / mobile / OTA)
- API gateway (auth + rate limits)
- Search with an availability cache
- Booking service (stateless)
- Room-night inventory — counts per (room type, night)
- Bookings DB (ACID)
- PSP integration — tokenize now, charge later
- Outbox → events to the property
Senior+ additions
Unprompted.
- Overbook ceiling as per-night data, walk policy named
- Hold-expiry sweeper on PENDING bookings
- Night audit: no-shows → fees → forecast feedback
- Property-local time discipline everywhere
- Cache invalidation on sellout, not just TTL
Required flows
- Search reads the cache; the ledger never serves a search
- Reserve all N nights in one transaction, in date order
- Rowcount == N or roll back — no partial stays
- Reserve before guarantee; charge only at stay / no-show / late cancel
- Confirm + outbox in one ACID transaction
- Desk-side changes re-enter through the same conditional update
Forbidden flows — penalized on sight
- Check availability, then insert the booking (read-then-write)
- A rooms table with one row per physical room on the booking path
- Charging the card at booking on guarantee rates
- A hard never-oversell rule — or an unbounded one
- Search traffic locking inventory rows
- Holds without expiry
The request path, narrated — this is your whiteboard script
- The gateway authenticates and splits the worlds: availability requests go to search and its cache; the book request continues inward.
- Booking — stateless — wins the atomic idempotency claim. From here a double-click and a timeout retry are the same booking.
- One transaction reserves every night of the stay: one conditional UPDATE per (room type, night) row, in date order, each checking reserved < sellable. Fewer than N rows matched → roll back, clean “no longer available.”
- The guarantee attaches — a card token for most rates, a capture only for prepaid. Nothing else is charged tonight.
- One ACID transaction confirms the booking and writes the outbox event; the hold stops being a hold.
- After the commit: the PMS learns the booking, and the night audit — property-local — sweeps expired holds, bills no-shows, and feeds the forecast that sets tomorrow’s ceiling.
One Postgres holds inventory, bookings, rate plans, and the outbox — which is exactly what keeps an N-night reserve a single ACID transaction. ~35 bookings/sec against a ~90 GB nightly ledger is comfortable for years: partition inventory by month, archive nights in the past, replicate for reporting. Sharding by property is the move you defend not making — it buys nothing at this write rate and costs you the multi-row transaction the whole design leans on.
Reserve N nights → attach guarantee → confirm, with the reserve as N conditional updates in one transaction — date-ordered, rowcount-checked — against a sellable ceiling deliberately above physical. Async begins after the commit.
07 · Deep dives · minutes 30–45
The three that decide the interview
1 — The three-night booking
The probe
“A guest books Friday to Monday — three nights. Two other guests want overlapping stays and there’s one deluxe king left on Saturday. Walk me through why nobody half-books.”
Your answer, in order
- Name the shape first: a stay is not one unit, it’s one unit per night — three rows for three nights. The naive build checks all three with a SELECT, sees them free, then inserts the booking: a TOCTOU race spread across rows. Two overlapping stays both pass the read, both insert, and Saturday oversells past even the overbook ceiling.
- The fix is checkout’s discipline applied N times inside one transaction: UPDATE room_night_inventory SET reserved = reserved + 1 WHERE property = ? AND room_type = ? AND night IN (fri, sat, sun) AND reserved < sellable — then check the rowcount. Three rows or roll back: a stay that books two of its three nights is corruption dressed as success.
- Now the part checkout never had: deadlock. Two overlapping stays that lock their nights in different orders can each hold a night the other needs. The discipline is a canonical lock order — always by date — so waits form a line, never a cycle. Say “I lock nights in date order” before the interviewer asks.
- And the loser exits cleanly at reserve time — “Saturday is no longer available,” with alternatives (shift the dates, change the room type) — before a card number was ever typed.
Staff extra — if pressed on isolation levels: the conditional UPDATE is atomic per row under plain read-committed — the WHERE re-evaluates under the row lock — so the design needs ordered locking and rowcount checks, not serializable isolation.
2 — Overbooking, on purpose
The probe
“Your e-commerce checkout answer says never sell what you don’t have. Now you’re telling me this hotel sells 216 rooms and owns 200. Defend it.”
Your answer, in order
- Own the inversion: in checkout, an oversell is a broken contract; in a hotel, an empty room-night is inventory that expired worthless — it can’t be restocked tomorrow. With ~8% no-shows on 200 rooms, capping at physical throws away ~16 sellable nights, every night. Overbooking is the business correcting for a measured statistic.
- The mechanism is one number: sellable = physical × (1 + overbook_rate), stored on the (room type, night) row. The book path doesn’t know overbooking exists — it runs the same conditional update against sellable. Policy lives in data; correctness lives in the update.
- The rate is a forecast, not a constant: set per property and per night from the no-show history the night audit feeds back — and set to zero on compression nights. Nobody no-shows the night of the concert; the statistics that justify the ceiling also say when to switch it off.
- And the failure is designed, not handled: when everyone shows up, the walk policy decides who (the last-arriving one-night stay, never the elite), what it costs (a partner room + transport + goodwill, ~2–3× the night’s rate), and where it’s logged — because walk cost is the error signal that says the rate was set too high.
Staff extra — the strongest close: reserved ≤ sellable is still a hard invariant — overbooking never means unbounded. You moved the ceiling; you didn’t remove it. “Controlled oversell with a priced failure mode” is the whole answer in six words.
3 — The night search lied
The probe
“A stadium show is announced at 9am. 50K fans hit search for the same weekend; your cache says available. The hotel has 200 rooms. Go.”
Your answer, in order
- Split the failure honestly: search telling 50K people “available” is not the bug — search is built to lie for seconds. The bug would be the book path believing it. The conditional update against sellable admits exactly 216 Saturday deluxe kings no matter how many people were told yes.
- So the real work is demand-shaping, not correctness: invalidate the cache on sellout — push, not TTL; a sold-out night must stop being advertised in milliseconds — return “no longer available” with live alternatives (nearby properties, shifted dates), and rate-limit per guest at the gateway.
- Contention stays local: 50K fans converge on a handful of (room type, night) rows at one property. Row locks serialize a few hundred short transactions a second without drama — this is checkout’s flash sale at a twentieth of the intensity, and saying so instead of reaching for Redis counters is the senior calibration.
- The policy layer reacts too: compression detected → overbook_rate to zero for that night, prices to revenue management, and the audit watching walk risk on the surrounding nights. The system’s best answer to the concert is mostly turning its own cleverness off.
Staff extra — if pressed on the cache: per-(property, room type, night) counters, decremented optimistically on booking events, rebuilt from the ledger on drift — the cache is a bouncer, the ledger is the law. The same division as checkout’s counter, at a fraction of the pressure.
Scenario player
The same design under fire. What breaks, what the naive build does, what the correct mechanism does.
critical
What breaks
A Fri–Mon stay and a Sat–Sun stay race for the last deluxe king on Saturday night.
What the naive design does
“SELECT the three nights, see them free, INSERT the booking.” Both reads pass, both insert — Saturday now holds more stays than even the overbook ceiling allows.
The correct mechanism
- One conditional UPDATE per night inside one transaction — the ceiling check lives in the WHERE, under the row lock.
- Rowcount == N or roll back: no stay half-books.
- Nights lock in date order, so overlapping stays queue instead of deadlocking.
- The loser hears “Saturday is gone” with alternatives — before entering a card.
Components involved
Rapid-fire
- Channel managers and OTAs are the same two-writer problem at fleet scale — name the allotment split, don’t design the sync protocol.
- Room assignment is fulfillment: bind the number at check-in, keep the booking on the count — an upgrade becomes a free move instead of a re-book.
- Long stays and corporate blocks reserve the same way — N rows, one transaction; a 30-night stay is the same code path as 3.
Three dives, one theme: the ledger is per-night counts, and every rule above it is data — the ceiling, the window, the walk. The book path stays one transaction; the policy layer stays a config.
08 · Traps & misconceptions
Seven claims that fail this interview
Each claim below is wrong. Read the claim, decide why it fails — then open it. These mirror the misconception patterns the simulator probes.
01 “model each physical room, and book room 204”
Why it fails — The booking path now juggles assignment, upgrades, and out-of-service rooms a year in advance — and a 3-night stay needs room 204 free on all three nights even when 205 would do. Assignment constraints leak into every availability query.
Instead — Count-based inventory per (room type, night); the room number is assigned at check-in. Inventory is arithmetic; assignment is fulfillment.
02 “never sell more rooms than the hotel has”
Why it fails — An unsold room-night expires worthless at midnight — with ~8% no-shows you’re discarding ~16 nights per 200-room hotel, nightly. This is the one system where refusing to oversell is the naive answer.
Instead — sellable = physical × (1 + overbook_rate) as per-night data, reserved ≤ sellable as the hard invariant, and a priced walk policy for the night the forecast loses.
03 “check that the nights are free, then insert the booking”
Why it fails — A TOCTOU race spread across N rows: two overlapping stays both pass the read and both insert. Multi-night makes it worse — you can also half-book a stay.
Instead — One conditional UPDATE per night in one transaction, date-ordered, rowcount == N or roll back.
04 “charge the card when the booking is made”
Why it fails — Most hotel rate plans are pay-at-stay with free cancellation windows — charging up front turns every cancellation into a refund and loses to every competitor’s “no prepayment” button.
Instead — Tokenize at confirm; charge at checkout, no-show, or late cancel per the rate plan. Prepaid is a rate-plan flag, not a different architecture.
05 “store availability as date ranges per booking”
Why it fails — Partial overlap turns every availability check into interval arithmetic over all bookings — “how many are free on Saturday?” becomes a scan instead of a read.
Instead — One counter row per (room type, night). A stay is N decrements; availability on any night is one row.
06 “make search strongly consistent so it never lies”
Why it fails — ~250 searches per booking, each fanning out across properties × nights, against rows the book path locks — you melt the ledger to freshen an answer the book path re-asserts anyway.
Instead — Cache search, budget its staleness, invalidate on sellout — and let the book path be the only truth.
07 “a no-show just frees up the room”
Why it fails — It leaks the fee the guarantee exists to collect, and it starves the overbook forecast of the exact data that sets the ceiling.
Instead — Night audit marks NO_SHOW property-local, charges the fee per the rate plan, releases the nights, and feeds the forecast.
Failure drills — would you catch it?
Four attack scenarios from the engine, tagged by severity. Commit to an answer before opening the fix.
critical Peak evening: multi-night bookings with overlapping date ranges start deadlocking. Each transaction locks its nights in whatever order the query planner picked, and the retry storm makes it worse.
The mechanism — A canonical lock order — always by date — turns lock waits into a queue instead of a cycle: one ordered multi-row UPDATE (or ordered SELECT … FOR UPDATE), plus bounded retry with jitter. Deadlocks between overlapping stays are a design smell, not a database mood.
critical The 8% overbook rate is a global constant. A stadium announces a Saturday show; no-shows that night are near zero, and the desk walks fourteen guests — including two top-tier members.
The mechanism — The rate is per (property, night), forecast-driven, and zeroed on compression nights. Detection inputs: event calendars, search-volume spikes, sellout velocity. The walk log is the error signal — a walk night without a forecast postmortem is the real failure.
high A guest books at 11pm, the hold expires while they fetch a card, and the sweeper releases the nights mid-payment. The guarantee attach succeeds against a booking whose rooms are gone.
The mechanism — Bounded hold extension while the guarantee is verifiably in flight, and confirm re-runs the conditional reserve: nights still there → confirm; gone → release the token with an apology. Checkout’s payment-after-expiry race, minus the refund — nothing was charged.
high Cancellation windows are evaluated in UTC. Guests in Sydney get charged fees six hours before their 6pm deadline; guests in Honolulu cancel free hours after theirs.
The mechanism — Every policy clock runs property-local: store the time zone on the property, evaluate windows and run the night audit against it, and print the local deadline in the confirmation. A reservation system’s bugs cluster where UTC meets the front desk.
09 · How this gets scored
The rubric, verbatim
Scored as: “Senior engineer on a travel / hospitality booking-platform team.”
Must-haves
- Models inventory as counts per (room type, night) — no physical-room rows, no date-range arithmetic on the booking path — and assigns room numbers at check-in.
- Reserves a multi-night stay atomically: conditional updates against the sellable ceiling in one transaction, date-ordered locking, rowcount == N or rollback — and names the TOCTOU race in check-then-insert.
- Treats overbooking as bounded policy: sellable = physical × (1 + overbook_rate) as data, reserved ≤ sellable as invariant, a walk policy with named compensation — and zeroes the rate on compression nights.
- Splits search from book — cached, fan-out, allowed-to-lie search against a strongly consistent book path — and does the arithmetic (~3M/day ≈ ~35 TPS) that keeps one ACID database the right answer.
Nice-to-haves
- A real hold story: PENDING as the hold with expires_at, a sweeper, bounded extension while the guarantee is in flight, re-reserve at confirm.
- The money-later model stated: guarantee vs prepaid rate plans, cancellation windows and no-show fees evaluated property-local, first charge often at checkout.
- The audit loop closed: no-show actuals feed the overbook forecast, and walk cost is the error signal on the rate.
Red flags
- Read-then-write availability anywhere on the book path, or a multi-night stay that can partially book.
- Refusing the overbooking requirement (“just never oversell”) or implementing it without bounds — both miss that it’s a tuned, priced policy.
- Charging cards at booking on guarantee rates, or evaluating cancellation windows in UTC.
The verdict ladder
Depth-ladder explorer
Four topics, four levels each — verbatim from the depth rubric. Slide L1→L4 and watch the same answer upgrade.
probed in: requirements, core entities, deep dive
L1 — surface
Mentions checking room availability before booking, without a concurrency story
L2
Proposes a transaction around the availability check and the booking insert
L3
Models one counter row per (room type, night), reserves N nights as conditional updates in one transaction with a rowcount check, and names the TOCTOU race in check-then-insert
L4 — staff+
Adds date-ordered locking against deadlock between overlapping stays, reasons about isolation (why read-committed plus conditional updates suffices), and prices false “unavailable” against oversell under retry storms
This question rewards engineers who see that the product sells room-nights and the design is a policy engine over a per-night ledger. A Strong Hire reserves N rows atomically in date order, encodes the overbook ceiling as tuned data with a priced walk plan, holds bookings without money, and lets search lie while book tells the truth. A Lean Hire has the ledger but hard-codes the policy. A No Hire checks then inserts, or half-books a stay. Weight multi-night atomicity and the overbooking judgment most heavily — the first is correctness under concurrency; the second is whether the candidate can encode a business rule they personally wouldn’t choose.
10 · FAQ
Common questions
- How hard is the hotel reservation interview question?
- Medium — each piece (per-night counters, a conditional update, a state machine) is simple; the difficulty is composition: three rows that must move together, a ceiling that’s deliberately above capacity, and money that arrives weeks after the booking. It’s a favorite senior screen because the overbooking requirement tests whether you can encode a business policy instead of arguing with it.
- Should I model individual rooms or counts per room type?
- Counts per (room type, night). A physical-rooms model drags assignment constraints into every availability check and makes multi-night queries interval arithmetic; the count model makes a stay N decrements and any night’s availability one row. The room number is fulfillment — bind it at check-in. Interviewers listen for this in the first ten minutes.
- How is this different from the e-commerce checkout question?
- They’re deliberate opposites. Checkout’s contract is never sell what you don’t have; a hotel sells 216 rooms on 200 on purpose, because unsold room-nights expire worthless and ~8% of guests don’t show. Checkout’s atomicity is one row; a stay is N rows that succeed or fail together. And checkout charges at order time, while most hotel bookings hold no money until the stay.
- Do I charge the guest’s card when they book?
- Usually not. Most rate plans are card-guarantee: tokenize the card at confirmation, then charge at checkout — or charge the no-show or late-cancellation fee per the rate plan’s rules. Prepaid discounted rates capture up front, but that’s a flag on the rate plan, not a different architecture. Charging everyone at booking turns every cancellation into a refund.
- How should I budget the 45 minutes?
- Roughly: 8 min requirements (state the inverted contract — bounded overselling), 5 estimation (~35 bookings/sec; say why you won’t shard), 7 entities + API (the per-night row and the hold), 10 high-level design (the search/book split and the one-transaction reserve), 15 deep dives — multi-night atomicity, the overbooking policy, and the hot night decide the verdict.
You’ve read the answer. Could you defend it?
Every candidate who fails this question has read a page like this one. The interview isn’t recall — it’s 45 minutes of follow-ups against your specific design, live. The simulator asks this exact question, probes the same multi-night and overbooking seams, and scores you on the same rubric — then tells you, honestly, where you actually stand.
Free 15-min session · No card · AI interviewer, honest verdict