Skip to content

03. Hotel / Stay Booking — Interview Q&A

~15 min read · Part 4 of 4 (Overview → HLD → LLD → Q&A)

These are the questions an interviewer actually asks once the two lanes are on the board. Each answer is the strong version, followed by the wrong answer that quietly sinks candidates.

Q1. Two guests try to book the same room for overlapping dates within the same second. How do you guarantee exactly one wins? Model inventory as one row per room-night with a unique constraint on (listing_id, night), and let the database's constraint be the arbiter. To book Dec 24–27, insert the three night-rows for the 24th, 25th, and 26th in a single single-shard transaction; if any night is already owned, the constraint aborts the whole transaction and that booker gets a 409 with the conflicting nights. When Guest A (24–27) and Guest B (25–28) collide on LST-42, whichever commits first owns the 25th and 26th, and the other's insert violates the constraint and rolls back atomically — no partial hold, no double-sell, decided in a few milliseconds. The point is that the check and the claim are the same atomic operation, so there is no window where both observe the nights as free. Common wrong answer to avoid: "Read the calendar to check it's free, then write the booking." That check-then-act has a race: both requests read "free" and both write. Availability checks are advisory; only the constrained write is authoritative.

Q2. Why not just take a lock on the listing for the duration of the booking? Because the booking includes a slow external payment, and holding a lock across it serializes the entire hot listing behind one guest's card processor — during the New Year peak, 40 bookers would queue behind each ~1.2 s payment. Double-booking is not a locking problem, it's a uniqueness problem: commit a short-lived HELD night-row guarded by the unique constraint, return immediately, and run the payment outside any lock. The committed hold plus the constraint already guarantees no second booker can take the night, so there is nothing to gain from holding a lock and a lot of throughput to lose. Common wrong answer to avoid: "SELECT ... FOR UPDATE the listing row and hold it through payment." It's correct but it collapses concurrency on exactly the listings that need it most, and a slow or hung payment freezes the whole listing.

Q3. Should search read the live availability calendar so results are always accurate? No — that couples a 12,000-searches/second read path to the transactional store and forces every one of ~1M daily bookings to update the search index, with hot listings churning constantly and still racing the booking that just happened. Split the lanes: index only a coarse availability signal (which months have any open nights), let search over-return plausible candidates, and confirm exact availability at the detail and reserve steps. A booked room showing up in search is not a data-integrity bug, it's a cache miss you resolve at reserve time. This keeps the index cheap and lets it lag a few seconds without any risk to correctness. Common wrong answer to avoid: "Query the calendar per result to filter out booked listings." That fans every search out to the transactional store and destroys both search latency and the store's isolation, to buy accuracy the reserve step already enforces.

Q4. Where does the availability calendar's 3.65 billion room-nights live, and how do you avoid storing them all? 5M listings over a 730-day horizon is ~3.65B potential cells, but you never materialize them. Store a row only when a night is spoken for — a HELD, CONFIRMED, or BLOCKED row — and treat "available" as the absence of a live row. A range is free if no owning row overlaps it, and an expired hold is made invisible by a hold_expires > now() predicate so a night frees up instantly without waiting for the reaper to delete anything. The table then holds only real bookings and blocks, tens of GB, not hundreds, and the reaper's deletion is garbage collection rather than correctness. Common wrong answer to avoid: "Pre-create a row per listing per night and flip a boolean." That materializes billions of rows, most of them available, and turns every host-calendar edit and every booking into a churn of updates over dead space.

Q5. Payment is slow and can fail. How do you hold inventory across it without freezing the room? Split reserve into two phases. A hold commits the night-rows in state HELD with a short expiry (~10 min) and returns immediately — the guest owns the nights, guarded by the unique constraint, with no open lock. Then confirm charges the payment gateway and, on success, flips the rows to CONFIRMED. Because the hold is a committed row and not a lock, a slow or hung payment blocks nobody else; if the guest never pays, the reaper releases the expired hold and the nights return to inventory. Confirm is idempotent via an idempotency key so a retried request charges the card exactly once. Common wrong answer to avoid: "Charge the card first, then write the booking." If the write then fails you've taken money for a room you can't confirm, and you still have the double-book race on the write itself.

Q6. If Guest A holds the nights and their payment hangs, what happens to Guest B who wanted overlapping dates? At hold time B was already rejected with a 409, because A's hold committed the night-rows for the 25th and 26th first. B does not wait on A's payment at all — that's the benefit of committing the hold instead of holding a lock. If A's payment then hangs and A abandons checkout, A's hold expires after ~10 minutes, the reaper releases the 24th–26th, and those nights become bookable again; B (or anyone) can now win them. So B is told "gone" instantly and, if A fails to pay, the inventory recovers cleanly a few minutes later rather than being locked for the length of A's payment attempt. Common wrong answer to avoid: "B blocks until A's payment resolves." That reintroduces the lock-across-payment problem and makes the loser's latency depend on the winner's card processor.

Q7. How does dynamic pricing interact with a booking that takes minutes to complete? Quote the price at search/detail time from a pricing service (base rate plus demand, lead time, length-of-stay, local-event signals), return it with a quote_id and a short TTL, and freeze the quoted total onto the hold. The guest pays what they were shown even if demand moves the live price during their checkout. Quotes are cached in Redis for their TTL, so recomputing is cheap and a pricing-service outage falls back to the last cached quote or the base rate. The invariant is that price is decided once, at quote time, and carried immutably through hold and confirm. Common wrong answer to avoid: "Recompute the price at confirm from live demand." The guest then gets charged a different number than they agreed to, which is both a trust problem and, in many places, illegal.

Q8. Booking volume is only ~250/second at peak — so why is this a hard system? Because the difficulty is contention, not throughput. Global write load is trivial and no store struggles with 250 writes/second. The pain concentrates: during the New Year peak, 40 guests converge on LST-42's same three nights within seconds. The design's job is to make the losing case cheap — a single-shard constraint insert that succeeds or fails in a few milliseconds — so 39 losers get instant 409s instead of queueing. Sharding by listing_id isn't for throughput either; it's for isolation, keeping one hot listing's contention on one shard so it can't slow bookings for the rest of inventory. Common wrong answer to avoid: "Scale up the database / add more write replicas." Throughput was never the bottleneck; more machines don't help 40 people fighting over 3 nights, and write replicas can't serve the serialization point at all.

Q9. What's your shard key for the inventory store, and why? Shard by listing_id, so every night of a listing and all contention for it live on one partition. That makes the reservation a single-partition transaction — the unique-constraint insert never spans shards, so you avoid distributed transactions entirely, which is what keeps the collision resolvable at the speed of one local insert. It also isolates hot listings: a stampede on LST-42 is contained to its shard. Booking dominates by listing, and "my trips" is a secondary index, so nothing wants a different primary partition. Common wrong answer to avoid: "Shard by date" or "shard by guest_id." Sharding by date scatters one listing's nights across shards and turns a range booking into a distributed transaction; sharding by guest puts the contended resource (the listing) on the wrong axis entirely.

Q10. The reaper that releases expired holds falls behind. What breaks, and how do you catch it? Expired holds don't get physically released, so inventory silently shrinks — phantom unavailability that starves both search and new holds while aggregate booking QPS looks perfectly healthy. Correctness is preserved (the hold_expires > now() predicate already treats lapsed holds as free at read time, so a hungry booker can still win the night), but if that predicate weren't there you'd be selling nothing on contended listings. Catch it by alerting on hold-table size and hold-age, run the reaper redundantly, and make release idempotent so a backlog drains safely. The graph to open first in a booking incident is hold-table growth and reaper lag, not throughput. Common wrong answer to avoid: "Watch the booking success rate / QPS dashboard." Aggregate metrics stay calm while specific hot listings melt; per-listing conflict rate and reaper lag are the signals that actually move.

Q11. The same room is listed on your site and two other OTAs. How do you avoid overselling it? Make one calendar the single system of record that every channel reserves against, and distribute availability/rate/inventory (ARI) outward to the channels via push rather than syncing independent copies and hoping they agree. Every reserve, from any channel, hits the same constraint on the same night-row, so the double-book guarantee holds across channels exactly as it does within one. Where true channel independence is unavoidable, you accept that sync lag creates a small oversell rate and handle it with relocation/compensation rather than pretending propagation is instant — but the default is one source of truth, not N copies. Common wrong answer to avoid: "Periodically sync availability between the channels." Any sync interval is a window for two channels to sell the same night; consistency has to come from a shared write point, not reconciliation after the fact.

Q12. How fresh does search have to be, and what's the cost of getting that wrong? A few seconds of staleness is fine, because search is advisory and the reserve step is authoritative. Over-freshness is expensive: making the index exactly live means every booking updates a document, hot listings churn constantly, and you pay reindex cost to prevent an error the reserve 409 already handles for free. Over-staleness is cheap in the right direction — a listing that got booked still showing as likely costs one wasted detail-page check, whereas the opposite mistake (hiding a free listing) loses a real booking permanently. So the design over-returns and tolerates lag deliberately, spending index budget on relevance and geo performance rather than on availability precision. Common wrong answer to avoid: "Keep the index perfectly consistent with the calendar." That's the most expensive possible choice to prevent the least harmful possible error.

Deeper follow-ups

  • How would you support "flexible dates" search (±3 days, or "any weekend in December") without materializing every date combination or fanning out to the calendar?
  • Multi-room properties (a hotel with 40 identical rooms): would you keep per-room-night rows, or a count per room-type-night, and how does the uniqueness guarantee change?
  • How would you implement waitlisting, so Guest B is automatically offered the nights if Guest A's hold expires, without polling?
  • How do you prevent a malicious client from holding scarce inventory with no intent to pay (hold-and-abandon as a denial-of-service on availability)?
  • If you needed globally low-latency search across regions, how would you replicate the search index and the calendar, and what consistency would you accept on each?
  • How would you handle time zones and DST for night boundaries when the guest, the listing, and your servers are in different zones?

How this round is scored

Interviewers use stay-booking to see whether you can hold two consistency models in your head at once. The strong signal is recognizing early that this is two systems — a stale-tolerant, read-heavy search path and a strongly-consistent, contention-bound booking path — and refusing to let one contaminate the other. The single highest-value move is resolving double-booking as a uniqueness constraint rather than a lock, and then noticing that the real reason it matters is the slow payment you must not lock across; candidates who get both have clearly built this before. Seniority shows in the tradeoff discussions — search freshness vs index cost, hold TTL vs conversion, one-calendar vs channel sync — where you name both sides and pick with a number. Reaching for "scale the database" when told the write volume is only 250/second is the classic tell that a candidate is pattern-matching on "big system" instead of reading the actual constraint, which here is local contention, not global throughput.