Interview Perspective
How to structure a ticket booking system design interview — what to say, what to demonstrate, where candidates go wrong, and a full question bank across scope, data model, concurrency, idempotency, holds, payment, availability, high-demand handling, messaging, reconciliation, multi-region, and failure curveballs.
How to Structure Your Answer
Open by naming the central tension: availability reads and reservation writes have different correctness requirements.
State early: 'A user browsing the seat map can see slightly stale data — that's fine. But the actual reservation must validate the authoritative seat state. These two workloads need different architectural solutions.' This frames the entire design correctly.
Interviewers often phrase this prompt around a named product — 'design Ticketmaster,' 'design a movie ticket booking app,' or 'design a seat reservation system for a stadium.' These are the same underlying problem with a different inventory shape (assigned seating vs. general admission) and a different demand pattern (a single on-sale spike vs. steady daily bookings). Name the pattern in your answer — don't let the brand in the prompt change the fundamentals you reach for.
Walk through requirements. Identify no-double-selling as the #1 invariant, alongside idempotent booking submission as a close second. Note availability tolerance for staleness vs reservation correctness. Establish that high-demand events can see traffic far above normal baseline — treat any specific multiple as illustrative, not a fixed fact.
Start simple: User → API → Booking Service → DB. Introduce the seat. Introduce two concurrent users. Show the race condition. Then introduce the fix — step by step. Once the single-seat, single-request case is solid, layer on holds, expiry, payment coordination, multi-seat requests, and finally high-demand handling.
Key Concepts to Demonstrate
The concepts that separate a strong ticket booking answer from a generic API + database answer.
The race condition
Name it. Show why read → decide → write is not atomic. This is the core problem. Candidates who skip this demonstrate they haven't thought deeply.
Atomic reservation
Name the mechanism — conditional update, optimistic concurrency, or row-level locking scoped to the short creation transaction only. Explain that only one request can succeed — others are rejected, not queued.
Durable hold identity
A hold is a record with its own identity (hold_id, owner, expires_at, version) — not just a flag on the seat. This identity is what makes expiry and payment coordination safe to reason about.
Idempotency, not just atomicity
Atomic reservation stops two different users from winning the same seat. It does not, by itself, stop one user's retried request from being treated as a second attempt. Say both, explicitly — they're different guarantees.
Expiration safety
The expiration vs. payment race, and the deeper generation-protection problem: an old, delayed expiry task must never release a seat that a newer hold now legitimately owns. Condition release on hold identity, not just seat state.
Payment ambiguity as a first-class case
Payment success/failure can be unknown or arrive late. State the explicit policy: never silently guess, never let a late success steal a seat that was legitimately reallocated, always reconcile toward an agreed final state.
Availability vs reservation separation
Cache availability reads. Never cache for reservation decisions. Explicitly state this distinction.
Admission control and partitioning under high demand
Rate limiting / a waiting room protects the booking path. Availability cache absorbs read load. Partitioning below the event level protects against one blockbuster event saturating a single shard.
- 01Name the race condition explicitly before proposing the fix
- 02Atomic reservation: only one can succeed — state this clearly
- 03Idempotency is a separate guarantee from atomicity — say both
- 04A hold is a durable, identified record — not a boolean flag on a seat
- 05Expiry must be scoped to hold identity, never just seat state
- 06Payment ambiguity needs an explicit policy, not a silent assumption
- 07Availability reads ≠ reservation guarantee — separate these architecturally
- 08Admission control and partitioning protect the authoritative path under spike traffic
- 09"I would not rely on a cached seat state to make the reservation." — say this
Common Mistakes
Mistakes that reveal incomplete thinking about concurrency, idempotency, and payment correctness.
Not naming the race condition
Designing the system without demonstrating awareness of why double-booking happens. The race is the entire problem.
Using cached availability for reservation
Treating a cached 'AVAILABLE' value as sufficient to proceed with booking. This is the exact source of double-booking.
No hold expiration, or expiration that checks only seat state
Creating holds without a release mechanism is one failure. A subtler and more common one: releasing based only on 'is this seat HELD' rather than 'does this seat still belong to this specific hold' — which lets a delayed expiry task steal a seat from a newer hold.
Confusing atomicity with idempotency
Believing that a conditional atomic update alone prevents duplicate bookings. It prevents two different users from winning — it does not prevent one user's retry from being treated as a new attempt without a separate idempotency key.
Boolean booking state
Using booked=true/false without explicit states. Cannot represent HELD, PAYMENT_PENDING, or the distinction between 'hold expired' and 'payment failed' — and cannot represent hold state, booking state, and payment state as the related-but-distinct things they are.
Assuming the hold TTL guarantees payment resolution
Treating the hold's expiry window as a guarantee that reconciliation will always finish first. It is not a guarantee — payment can resolve after the hold has already expired, and the design needs an explicit answer for that case rather than an assumption it won't happen.
Row-locking through the payment window
Holding a database row lock from reservation all the way through the customer's payment step. The lock should protect only the short transaction that creates or updates the hold — never a multi-minute wait for a human to pay.
Treating a waiting room as a reservation guarantee
Conflating admission (being let into the booking flow) with reservation (actually owning a seat). An admission token proves the right to attempt a reservation, nothing more.
Scope & Data Model Questions
Questions that test whether you can scope the problem correctly and choose a data model that actually supports the invariants you'll need later.
How would you scope this problem in the first five minutes?
Clarify assigned-seat vs. general-admission inventory, whether multi-seat booking is in scope, whether payment is built or integrated, and whether high-demand/waiting-room behavior is expected. Then state the #1 invariant (no double-sell) and the close second (idempotent submission) before designing anything. See Problem Definition.
- What if the interviewer says 'assume general admission only'? — The per-seat atomic transition becomes an atomic bounded-decrement on a capacity counter — same concurrency-safety requirement, simpler mechanics, no per-seat identity to manage. See General-Admission Inventory.
What does your core data model look like for an event, a seat, and a hold?
An event owns seats (or a capacity counter for general admission). Each seat carries authoritative state. A hold is its own durable record — hold_id, event_id, owner, seat IDs, state, created_at, expires_at, version — not a flag on the seat. A booking references a hold and carries its own state plus a payment reference.
- Why not just put a 'held_by' column on the seat and skip a separate hold table? — You can fold hold fields onto the seat row for a single-seat model, but a separate hold record is what naturally supports multi-seat holds and gives you one clean idempotency key across several seats. See Seat State vs. Separate Hold Record.
Why does the hold need its own identity instead of just a state flag on the seat?
Because expiry and payment coordination both need to distinguish 'this specific hold' from 'whatever hold currently occupies this seat.' Without an identity, a delayed operation from an old hold can't tell that the seat has moved on to someone else. See Hold Data Model.
- Where does this show up as a real bug if you get it wrong? — See Old Expiry Task Releases a Newer Hold.
Concurrency & Idempotency Questions
Questions probing the core correctness mechanics — atomic transitions, locking choices, and idempotent retries.
Walk me through exactly what happens when two users try to reserve the last seat at the same instant.
Both requests attempt a conditional atomic transition (AVAILABLE → HELD) scoped to that exact seat. The database guarantees only one transition succeeds; the other affects zero rows and is rejected cleanly. See Atomic Seat Reservation.
- Optimistic concurrency or row-level lock — which would you pick, and why? — Depends on contention shape and how long the transaction needs to hold state. See Locking vs. Optimistic Concurrency.
Is retrying a failed reservation attempt ever a good idea?
Only if the conflict was transient (e.g. a version mismatch that has since resolved and the seat is genuinely still open). Retrying after losing a straight ownership race on a unique seat is not useful — that seat is gone. Distinguish the two before deciding to retry.
A client double-clicks 'Reserve.' Walk me through what should happen.
The second request carries the same idempotency key as the first. The system resolves it against the durable idempotency record and returns the existing hold rather than attempting a second reservation. See Hold Idempotency.
- What if the first request's response was lost, but it actually committed? — Same mechanism handles it — the retry discovers the existing hold via the idempotency key and returns it as if it were the original response. See Hold Created, Response Lost.
How do you reserve three seats together atomically, and what can go wrong if you don't design for it?
Attempt all three within a single transaction and verify all succeeded before committing any — otherwise a mid-sequence conflict leaves the user with an unwanted partial booking. Use a deterministic acquisition order across all requests to avoid deadlocks between two overlapping multi-seat attempts. See Multi-Seat Reservation.
- Why does lock order matter if each individual seat lock is still atomic? — Two multi-seat transactions can each hold one seat and wait on the other's, producing a deadlock, if they acquire seats in different orders. See Multi-Seat Deadlock.
Hold & Payment Questions
Questions about hold expiry safety and the payment/hold boundary — the two areas most likely to expose shallow thinking.
How do you make sure an expiry sweep never releases the wrong hold?
Condition the release on the specific hold's identity and version, not just 'is this seat marked HELD.' A delayed task from an old, already-expired hold will find the seat's current hold_id no longer matches and will safely do nothing. See Expiry Generation Protection.
- What if two expiry workers process the same expired hold at once? — Both attempt the same conditional release; only one affects a row, the other is a safe no-op by construction. See Multiple Expiry Workers Running Concurrently.
Payment succeeds, but a moment earlier the hold expired and the seat was already reallocated. What do you do?
Never take the seat back from whoever now legitimately owns it. Void or refund the late-successful payment, flag the original booking as a reconciliation exception, and notify the customer per policy (alternate seat, refund, or both). This is a defined policy decision, not an incidental edge case. See Payment/Hold Boundary and Payment Success, Seat Already Reallocated.
- Does a longer hold TTL solve this? — No — there's no TTL long enough to guarantee reconciliation always completes first. Design the late-success recovery path deliberately instead of trying to outrun the race with a bigger number.
Should the database row stay locked while the customer is on the payment page?
No. The lock should only span the short transaction that creates or updates the hold record. Locking through the payment window would tie up database resources for minutes per in-flight checkout and serialize unrelated work. See Atomic Seat Reservation.
How do you handle a duplicate payment webhook, or one that arrives out of order?
Deduplicate on the provider's event ID for duplicates. For out-of-order events, validate every incoming event against the current state's valid-transition table so a stale event can never regress an already-terminal payment or booking state. See Payment Webhook Duplicate and Payment Webhook Out of Order.
Availability & High-Demand Questions
Questions about caching strategy, admission control, and what actually breaks under a hot on-sale.
How aggressively should you cache the seat map, and what happens when it's wrong?
Cache against an explicit freshness target, not 'as aggressively as possible.' A stale-positive (shown available, actually taken) fails safely at reservation time. A stale-negative (shown taken, actually free) just costs a missed opportunity. Neither is a correctness bug as long as reservation always re-checks authoritative state. See Availability Cache Strategy.
A blockbuster event goes on sale and one shard is getting hammered while the rest of the platform is fine. What's happening and what do you do?
Sharding by event_id isolates events from each other but doesn't help when the load is concentrated on a single event — the whole event still lands on one shard. Fix it with finer partitioning (event → section/block → seat) plus admission control specific to that event. See High-Demand Partitioning and Hot-Event Partition Saturation.
- Does finer partitioning have a cost? — Yes — multi-seat requests that span more than one partition become harder to reserve atomically. It's a genuine trade-off, not a free win.
Would you reject excess reservation attempts outright, queue them, or use a waiting room? How do you decide?
Depends on the product's tolerance for user-facing wait and the cost of a rejected attempt. A waiting room paces admission ahead of the correctness-critical path; a queue defers processing; a straight reject is simplest but harshest under a real spike. See Queue vs. Reject vs. Waiting Room.
- Does an admission token guarantee the user gets a seat? — No — it only proves the right to attempt a reservation. Conflating admission with reservation is a common mistake. See Virtual Waiting Room.
Messaging & Reconciliation Questions
Questions about how confirmation propagates to tickets and notifications, and how the system heals when things don't resolve cleanly on the first try.
How do you guarantee a customer eventually gets their ticket after a confirmed booking, even if the process crashes right after committing?
Write the booking-confirmed event to an outbox table in the same transaction that commits CONFIRMED, so the event's existence is guaranteed by that same commit. A relay process publishes it asynchronously and may duplicate — every downstream consumer, including ticket issuance, must be idempotent. See Transactional Outbox and Ticket Issuance.
- Why not just call the ticket service directly from the booking transaction? — See Direct Publish vs. Outbox — a direct call can succeed while the DB commit fails, or vice versa; the outbox ties both to one atomic unit.
What happens if the ticket-issuance consumer processes the same booking-confirmed event twice?
A stable business idempotency key on the ticket record (e.g. a uniqueness constraint on booking_id + seat_id) means the second attempt recognizes the existing ticket and no-ops rather than minting a duplicate. This matters especially wherever tickets are scanned for admission. See Duplicate Ticket Generation.
How do you find and fix bookings stuck in an ambiguous payment state?
A reconciliation worker periodically queries payment-provider truth for bookings stuck past a threshold, applies the same guarded state-transition rules as the live path, and never confirms/cancels blind. Track both the oldest-pending-case age and total mismatch count as first-class metrics so a growing backlog is visible before it becomes a customer-facing problem. See Booking Reconciliation and Reconciliation Backlog.
Multi-Region & Failure Curveballs
Harder follow-up questions probing regional failover and general system resilience under failure.
Could you run this system active-active across two regions?
Not for free — two regions must never independently believe they each own the same scarce seat. A single-writer-per-partition model with a defined failover path is the safer default; true active-active requires deliberate fencing design and is an advanced extension, not a starting point. See Single-Region Owner vs. Multi-Region Active-Active.
- What has to be true for failover to be safe? — Clear write-authority ownership per partition, and fencing that prevents the original region from resuming writes unexpectedly after it recovers. See Regional Failure.
Your expiry worker has been down for twenty minutes during a high-demand on-sale. What's the impact, and how do you recover?
Expired holds accumulate without being released, so the read model can show seats as unavailable that are actually free again — a UX/inventory-visibility cost. Reservation and confirmation logic must still treat expires_at as authoritative regardless of whether the worker got to it yet. Recover by processing the backlog in bounded batches once the worker is back, not one giant sweep. See Expiry Worker Down.
A booking is cancelled after its ticket was already issued — maybe even already scanned at the door. How do you handle it?
Cancellation must invalidate/revoke the issued ticket as part of the same workflow wherever ticket validity is checked at the point of use. A ticket already used before cancellation is processed is a policy exception (no refund, manual review) rather than a silent gap. See Cancellation & Refund and Cancellation vs. Ticket Use.
Demand this year is 50x higher than last year for one event. What would you actually change, versus what would you say is out of scope for a 45-minute interview?
In scope: admission control sizing, finer partitioning for the hot event, cache capacity for the browse spike. Reasonable to flag as illustrative/out of exhaustive detail: exact numeric capacity targets, which should come from load testing the specific infrastructure rather than a rule of thumb. Say this explicitly rather than inventing a precise number under pressure.
Quick revision
| Topic | One-line answer |
|---|---|
| Core invariant | At most one live allocation (hold or confirmed booking) per seat per event; name the race, then fix it with an atomic conditional reservation. |
| Idempotency | A separate guarantee from atomicity: a stable key means retries and double-clicks reuse the same hold. |
| Hold | A durable record with its own identity, not a boolean on the seat; expires_at defines validity regardless of cleanup. |
| Expiry | Release scoped to hold identity and state, never seat state alone, so old tasks and multiple workers are safe no-ops. |
| Multi-seat | All-or-none in one transaction, seats acquired in deterministic order to avoid deadlock. |
| Payment ambiguity | Explicit policy for late or unknown payment; never steal a seat now owned by someone else; reconcile. |
| Read vs write | Cached availability for browsing; the reservation path never trusts the cache. |
| Spike traffic | Waiting room sized to the reservation ceiling, finer-than-event partitioning for a blockbuster, fail fast once sold out. |