03. Ticketing System — Interview Q&A¶
~15 min read · Part 4 of 4 (Overview → HLD → LLD → Q&A)
These are the questions an interviewer asks once the seat map is on the board. Each answer is the strong version, followed by the wrong answer that quietly sinks candidates.
Q1. How do you guarantee a seat is never sold twice under heavy contention?
Make each seat acquisition a single atomic conditional write and back the durable sale with a uniqueness constraint. Acquiring a hold is a version-guarded CAS — UPDATE seat SET state='held', version=version+1 WHERE seat_id=? AND state='available' AND version=? — so of thousands racing for the same seat, exactly one affects a row and wins; the rest match zero rows and get an instant 409. The final sale sets booking_id under a UNIQUE (event_id, seat_id) index, so even if every layer above malfunctioned the database physically refuses a second booking. Two independent guards — the CAS and the unique index — both have to fail before a double-sell is possible.
Common wrong answer to avoid: "Read the seat, check it's free, then mark it sold." That check-then-act has a race window where two requests both read "free" and both write — the exact bug the question is testing for.
Q2. Pessimistic or optimistic locking for the seat?
Optimistic, decisively, because a hold lasts up to 8 minutes. A pessimistic SELECT ... FOR UPDATE or distributed lock held for the whole shopping session throttles a seat to one attempt every 8 minutes and lets a fan who closes their laptop hold the row until something reaps it. Optimistic concurrency lets thousands attempt the same seat in parallel; the CAS resolves the winner in microseconds and losers are told immediately to reselect. Distinct seats never contend at all — only the same seat serializes, and only for the duration of one conditional write. The memory hook: a hold is not a lock, it is a lease — a short, self-expiring claim, not a mutex a client can hold open.
Common wrong answer to avoid: "Lock the seat row while the user is on the checkout page." That converts an 8-minute human decision into an 8-minute database lock, collapses throughput, and strands seats when clients vanish.
Q3. 100,000 fans hit buy at 10:00:00 for 10,000 seats. Walk me through how the system survives it.
Recognize first that this is contention, not raw traffic: 100,000 requests fighting over 10,000 rows, not over bandwidth. The virtual waiting room absorbs the burst — the 10,000 joins/second at 10:00:00 hit only a Redis sorted set (a cheap ZADD), and fans get honest positions instantly. The room then admits fans in arrival order, capping active shoppers near 1.5× remaining seats, so the reservation engine sees roughly a 2,000/s opening burst of hold attempts, not 100,000 at once. Hot seats like front-row A1 draw a few thousand CAS attempts each; one wins, the rest get instant 409s and reselect. Seats churn as ~30% of holds lapse at their 8-minute TTL and recycle, so total attempts (~25,000) exceed seat count while sold seats never exceed 10,000. Around the four-minute mark the last seat sells, available_count hits zero, admission stops, and every remaining queued fan — number 40,000, number 90,000 — is admitted straight to a clean sold-out response, never a dead seat map.
Common wrong answer to avoid: "Auto-scale the seat service to handle 100,000 requests." You cannot scale your way out of contention on a single row — adding servers just means more machines racing for A1. The fix is admission control plus atomic acquisition, not horsepower.
Q4. Why a virtual waiting room? Why not just let everyone onto the seat map? Because letting 100,000 fans onto a 10,000-seat map manufactures 90,000 guaranteed losers who generate contention, retries, and thundering-herd load on the hottest seats — all for seats that cannot exist for them. The waiting room converts a stampede into a metered, roughly-fair stream: it accepts everyone cheaply, orders by arrival, and admits only as many as there are realistically seats for (near 1.5× remaining). It gives backpressure (the reservation engine never sees more than it can serve), fairness (arrival order, not fastest-bot-wins), and honesty (a real position instead of a frozen page). It also drains cleanly — when inventory hits zero, the queue tail is admitted to a sold-out answer rather than left spinning. Common wrong answer to avoid: "The waiting room is just a nice-to-have UX spinner." It is a load-shedding and fairness mechanism; without it the seat engine takes the full 10,000/s burst directly onto its hottest rows.
Q5. What exactly is a hold, and what happens if the fan abandons it?
A hold is a time-boxed lease on a seat, not a sale and not a lock. Acquiring it flips the seat to held via CAS and writes a Redis key with an 8-minute TTL (SET ... EX 480 NX). If the fan pays in time, the hold promotes to a sold booking; if they close the tab or their payment hangs, the lease simply expires and a reaper flips the seat back to available, returning it to the pool. Nothing strands. Over-release is the safe failure direction — a seat wrongly freed can be re-sold, whereas a seat wrongly sold twice cannot be un-sold — so the whole design leans toward releasing on doubt.
Common wrong answer to avoid: "Hold the seat until the user manually cancels." A hold with no TTL means one crashed browser removes a seat from sale permanently; the self-expiring lease exists precisely to make abandonment self-healing.
Q6. Set the hold TTL. What breaks if it's too long or too short? Around 8 minutes, tuned against payment latency. Too short — say 90 seconds — and legitimate fans lose seats mid-payment when a card 3-D-Secure step or a slow gateway eats the window, so conversion drops and seats churn needlessly. Too long — say 30 minutes — and during our on-sale the 10,000 seats sit locked in holds by the first admitted wave, 30% of whom never pay, so 3,000 seats are dead for half an hour while 85,000 queued fans wait behind holds that will lapse anyway. The TTL must comfortably exceed real payment time (a few minutes) but stay short enough that lapsed holds recycle fast. Critically, the payment timeout must be shorter than the hold TTL, so a hung payment releases the seat rather than leaving it dead until the hold expires. Common wrong answer to avoid: "Whatever's convenient, like an hour." A long TTL turns a hot on-sale into a slow-motion deadlock where inventory is locked by non-buyers while real buyers wait.
Q7. Payment succeeds but the booking write fails. What now?
This is the split-brain to guard explicitly: money moved, no seat recorded. Write a payment_intent before charging and tie the payment to the booking through the idempotency key, then reconcile asynchronously — if a settled payment has no booking, either complete the booking (the seats are still held under the same hold_id) or refund and release. Because the idempotency key maps a payment to at most one booking, reconciliation is deterministic rather than a guess. The seat itself is never at risk of a double-sell here, because the seat only becomes sold when the booking row commits under its unique index.
Common wrong answer to avoid: "Just charge the card and then create the booking." Treating charge-then-write as one implicit atomic step ignores that the two systems can't commit together; without a payment-intent record and reconciliation you get silent double charges or paid-but-seatless customers.
Q8. How do you make checkout idempotent against double-clicks and retries?
Require a client-generated idempotency key on checkout and enforce UNIQUE (idempotency_key) on the booking table. A double-click or a network-timeout retry carries the same key, so the second insert conflicts and returns the already-created booking rather than charging again or claiming a second seat. Pair it with the promote step asserting WHERE hold_id=? AND state='held', so a retry after the hold expired returns 410 cleanly instead of resurrecting a released seat.
Common wrong answer to avoid: "Disable the buy button on the client after one click." Client-side guards don't survive retries, refreshes, or two tabs; idempotency has to be enforced server-side by a uniqueness constraint.
Q9. How do you keep the seat map live for thousands of shoppers without hammering the store? Push, don't poll. The client reads the full seat map once on admission, then holds an SSE or WebSocket connection and receives only deltas — seat A1 went held, seat B4 went sold. The reservation service publishes a delta once per state change and the push tier fans it out to all admitted connections. For our sale, only ~10,000 seats change state over four minutes, roughly 40 deltas/second broadcast, versus the 5,000 reads/second that 15,000 shoppers polling every three seconds would generate. The store serves one read per session, not a continuous poll. Common wrong answer to avoid: "Have the client poll the seat map every second." That multiplies your read load by the number of active shoppers and puts a continuous query stream on the same store that's handling contended writes.
Q10. When and how do you shard, and what's the partition key?
Shard by event_id. One event's inventory is tiny (~1.5 MB for 10,000 seats) but intensely hot for a few minutes, so the goal of sharding is isolation, not capacity: a stampede for Concert A must not degrade the seat engine serving Movie B. Each event's hot rows and holds live together on one shard, and you scale across many simultaneous on-sales by spreading events over shards. A single mega-event that outgrows one shard partitions further by seat section, since section 100 and section 200 never contend with each other.
Common wrong answer to avoid: "Shard by seat_id across all events" or "shard by user." Splitting one event's seats across shards scatters a single on-sale's contention and complicates the atomic multi-seat hold; user-based sharding doesn't match the access pattern at all.
Q11. An admitted fan waits too long and everything sells out while they hold a session. What do they see?
A clean sold-out. Admission never guarantees a seat — only a successful CAS does — so a dawdling fan's hold attempt runs the conditional update, finds no seat in available state, and returns sold-out immediately. This is deliberate: guaranteeing a seat at admission would mean reserving inventory for shoppers who may never buy, which either oversells or wastes seats. The session token is permission to try, not a claim on a seat.
Common wrong answer to avoid: "Reserve a seat for everyone we admit." With 15,000 admitted and 10,000 seats, that's an immediate contradiction; admission is a rate limiter, not an allocation.
Q12. Redis holds the leases and it goes down mid-sale. Do you oversell?
No — losing leases fails in the safe direction. The durable sold rows and the booking table survive in the relational store, so nothing already sold is forgotten. Lost held leases simply mean those seats' TTLs vanish; on recovery the seat map is rebuilt from durable rows (sold stays sold) and any seat not backed by a live booking defaults to available, so an in-flight hold is at worst released early and re-offered. The one thing that cannot happen — a seat sold twice — is protected by the relational unique index, which never lived in Redis. Over-release is recoverable; over-sell is not, and the durable backstop guards exactly the irrecoverable case.
Common wrong answer to avoid: "If Redis is down, we can't sell tickets" — or worse, "we keep selling from memory and reconcile later," which risks selling a seat that was already sold. The durable store, not the cache, is the arbiter of sold.
Deeper follow-ups¶
- How would you support best-available auto-selection (fan asks for "4 seats together, cheapest") without creating a hot path that scans and locks large seat ranges?
- How do you detect and throttle bots that flood the waiting room to grab better queue positions, while keeping real fans fair?
- If an event spans multiple regions of buyers, how do you keep one authoritative seat inventory while serving low-latency browsing globally — and what consistency do you accept where?
- How would you extend holds-and-leases to general-admission (no assigned seats, just a capacity of 10,000) versus reserved seating, and where does the CAS become a counter decrement?
- How do you handle a partial refund that returns one seat of a four-seat booking back to the available pool safely?
- What changes if the payment gateway supports only eventual settlement (bank transfer) rather than instant authorization — how long can a hold realistically live?
How this round is scored¶
Interviewers use the ticketing system to see whether you recognize that the hard part is contention and correctness, not throughput. The strong signal is naming early that 100,000-over-10,000 is a race for a small pool of rows, then building the two defenses that matter — admission control to shed the burst fairly, and atomic acquisition to resolve the race without a lock — rather than reaching for auto-scaling that does nothing for a hot row. Seniority shows in the tradeoff discussions: optimistic versus pessimistic locking with the 8-minute hold as the deciding number, the hold-TTL tuning against payment latency, and the deliberate choice to fail toward over-release rather than over-sell. The failure-mode thinking separates people who have run an on-sale from those who have only drawn one — the payment/booking split-brain, the reaper-versus-late-checkout race, and Redis loss all have a right answer that keeps the no-double-sell invariant intact. Doing the back-of-envelope math out loud — the 10,000/s join burst, the ~2,000/s hold burst behind the queue, the 70% conversion that makes attempts exceed seats — and using it to justify the admission cap and TTL is what pushes an answer from correct to senior.