Ticketmaster System Design Interview: Selling Scarce Seats Without Double-Booking
When a stadium tour goes on sale, millions of fans hit one event in the same minute for a few tens of thousands of seats. Ticketmaster has publicly described selling large tours where demand outstrips supply by a wide margin, so the hard part is not steady traffic, it is a synchronized stampede against scarce, uniquely identified inventory. Every seat can be sold exactly once, and the system has to stay correct while shedding load. Concrete concurrency figures for a single on-sale are not published by Ticketmaster, so treat the demand numbers here as estimates.
A ticketing platform looks simple until one hot event goes on sale and a hundred thousand people fight over forty thousand seats in the same sixty seconds. The core requirement is that a given seat is sold to exactly one buyer, ever, even under massive concurrency, while the site stays up for everyone else. The design has three load-bearing pieces. First, a virtual waiting room that holds and paces arrivals so the booking backend only ever sees traffic it can handle. Second, a reservation model where selecting a seat places a short temporary hold that auto-releases if payment does not complete, so abandoned carts do not lock inventory forever. Third, an idempotent, serialized commit path where the transition from held to sold happens under a lock or a conditional write that a competing request cannot win twice. Around that sit the seat map, the payment integration, and a reconciliation loop that cleans up expired holds. Get the waiting room, the hold expiry, and the atomic sell right, and the rest is standard web engineering.
Asked at: Asked at Ticketmaster, StubHub, SeatGeek, BookMyShow, and broadly across FAANG and high-growth interviews as "design a ticket booking system" or "design a flash-sale reservation system." Uber, Airbnb, and OpenTable ask close variants because the reservation-with-hold pattern is the same.
Why this question is asked
Interviewers like this problem because it forces a candidate to reason about correctness under contention, not just throughput. Anyone can scale reads. The interesting question is what happens when ten thousand requests all want seat 14F at once, and the honest answer requires understanding locking, idempotency, and the difference between reserving and confirming. It also rewards load-shedding maturity: a strong candidate proposes a waiting room instead of trying to make the booking database absorb a million concurrent writes. Finally it tests judgment about consistency versus availability, since selling a seat is one place where you cannot hand-wave eventual consistency.
Requirements
Always clarify these in the first 5 minutes of the interview. Do not start drawing boxes until both lists are agreed.
Functional requirements
- Browse events and view an interactive seat map showing which seats are available, held, or sold in near real time.
- Select one or more seats and place a temporary hold that reserves them for a bounded window, typically a few minutes.
- Auto-release held seats back to inventory if the buyer does not complete payment before the hold expires.
- Complete a purchase that atomically converts held seats to sold and issues tickets, charging exactly once.
- Guarantee no seat is ever sold to two different buyers, even under extreme concurrency.
- Run a virtual waiting room for high-demand on-sales that admits fans to the purchase flow at a controlled rate.
- Support idempotent booking so a retried or duplicated request does not create a second order or a double charge.
- Handle general-admission inventory (a capacity counter, no assigned seats) alongside reserved-seating inventory.
- Send confirmation, delivery of tickets, and clear messaging while a fan waits in the queue.
Non-functional requirements
- Strong correctness on the sell path: overselling is unacceptable and must be prevented, not merely detected.
- Absorb sudden spikes of one to two orders of magnitude above baseline when a marquee event opens.
- Keep the waiting room and seat map responsive (sub-second reads) even while the on-sale is saturated.
- Keep hold-release latency tight so freed seats reappear quickly and are not stranded.
- High availability for browsing and queueing; the booking commit can trade a little latency for correctness.
- Defend against bots and scripted buying that would otherwise starve real fans of inventory.
- Auditability: every state change on a seat is traceable for disputes, refunds, and fraud review.
- Graceful degradation: if the payment provider slows down, holds and the queue should still behave sanely.
Back-of-envelope scale estimates
Show your math. Pulling numbers from thin air signals you have not thought about the load.
Peak concurrent fans on a hot on-sale
~1,000,000 (estimate)
A stadium tour leg can draw far more interested buyers than seats. If an event has 50,000 seats and demand is 20x, roughly 1,000,000 people try in the opening minute. Ticketmaster does not publish per-event concurrency, so this is a demand estimate to size the waiting room.
Seats in the hot inventory
~40,000-60,000 per stadium show
Large stadium capacity minus staging, holds, and accessibility areas. This is the scarce resource the whole design protects. Correct sale count equals this number, no more.
Admission rate into the purchase flow
~1,000-5,000 fans/minute (tunable, estimate)
Derived from backend capacity, not demand. If the booking tier safely handles N seat-commit transactions per second with headroom, you admit at a rate below N. Public explainers describe throttled admission of roughly a thousand per minute as a common setting; the real number is a config knob.
Hold window (reservation TTL)
2-10 minutes (product choice)
Long enough for a human to pick seats and enter payment, short enough that abandoned carts free inventory fast. Ticketmaster has publicly referenced a roughly 10-minute shopping window after admission. Shorter windows increase effective inventory turnover during a sellout.
Seat-commit write rate at the database
hundreds to low thousands/sec (Derived)
This is capped by the admission rate, not by raw demand. That is the entire point of the waiting room: the commit tier never sees 1,000,000 concurrent writes, it sees a paced trickle it can serialize safely.
Seat-map read fan-out
millions of reads/min during on-sale (estimate)
Every waiting fan polls status and refreshes the map. Reads dwarf writes by orders of magnitude, so the map is served from a cache and a real-time push channel, decoupled from the transactional store.
High-level architecture
A fan requesting a hot event does not hit the booking service directly. The edge (CDN plus a lightweight queue service) first checks whether the event is under waiting-room control. If it is, the fan is issued a queue token and parked on a status endpoint that they poll or that pushes updates, showing position and estimated wait. A serving counter advances at a rate the backend can absorb; when the fan's turn comes, the queue service mints a short-lived signed token (a JWT works well) that authorizes entry to the purchase flow. Requests to the booking API without a valid token are rejected, which is what stops people from scripting straight past the line. Inside the purchase flow the fan loads the seat map, served from a cache and a real-time channel rather than the transactional database. Selecting seats calls the reservation service, which places a temporary hold: it writes a hold record with an expiry and flips those seats to held, using an atomic conditional operation so two simultaneous selectors cannot both succeed. Holds live in a fast store (Redis is a natural fit) with a TTL, and a delayed-queue or sweeper releases them on expiry. When the fan pays, the booking service performs the one transaction that matters, converting held to sold atomically, keyed by an idempotency key so a retry cannot double-book or double-charge. Payment runs against Stripe or an equivalent, and only a confirmed charge finalizes the order and issues tickets. Behind all this, an inventory store of record (a relational database) holds the durable seat state, while caches, the queue, and the hold store are the moving parts that keep the hot path fast and shed load.
In a real interview, sketch this on the whiteboard before diving into any single box.
Core components
Walk through each service. The interviewer wants to hear what each one owns, not just the names.
Virtual waiting room / queue service
Sits at the edge and decides who gets in and when. It assigns queue tokens, tracks a serving position, and admits fans into the purchase flow at a controlled, backend-safe rate rather than all at once. It issues a short-lived signed access token on admission, and the booking API refuses any request without one.
Seat map service
Serves the visual inventory for an event, marking each seat available, held, or sold. It reads from a cache and pushes live updates over a real-time channel (WebSocket or SSE) so fans see seats disappear as others grab them. It is deliberately decoupled from the transactional store to survive read fan-out during a sellout.
Reservation / hold service
Owns the temporary hold. On seat selection it atomically flips seats to held and writes a hold record with a TTL, so an abandoned cart auto-releases. It uses conditional writes or a lock so concurrent selectors of the same seat cannot both win, and it is the source of truth for what is currently reserved but not yet sold.
Booking / order service
Runs the commit that must be exactly right: held to sold, tickets issued, order created. It is idempotent on a client-supplied key so retries are safe, and it coordinates with payment so a seat is only marked sold once money is confirmed. This is the narrow serialization point the whole design protects.
Payment integration
Talks to Stripe, Razorpay, or an equivalent through a webhook-driven flow. The booking service authorizes or charges, then finalizes on a verified success callback, never trusting client-reported payment state. If payment fails or times out, the associated hold is released so the seats return to inventory.
Inventory store of record
A durable relational database that holds the authoritative seat, hold, and order state. It enforces the invariants that ultimately prevent overselling through constraints and transactional updates, and it is the system that reconciliation and audits trust when the fast stores disagree.
Hold-expiry sweeper
A background worker or delayed-queue consumer that releases expired holds back to available inventory. TTLs in the hold store cover most cases, but the sweeper guarantees the store of record and the seat map converge even if a TTL event is missed or a process crashes mid-hold.
Data model
Pick the right store per table. Justify each choice with the access pattern, not by reflex.
eventsevent_idvenue_idonsale_atstatuswaiting_room_enabledOne row per event. waiting_room_enabled and onsale_at let the edge decide when to switch a given event into queued mode. status covers scheduled, on-sale, sold-out, and closed.
seatsseat_idevent_idsectionrowseat_nostateprice_tierThe scarce resource. state is one of available, held, sold. A unique constraint on (event_id, section, row, seat_no) plus transactional state transitions are what make overselling structurally impossible.
holdshold_idevent_idseat_iduser_idexpires_atstatusA temporary reservation. Mirrored in a fast TTL store for the hot path and persisted here for audit. On expiry or checkout the row is resolved to released or converted. Also carries the general-admission counter decrements where seats are not assigned.
ordersorder_iduser_idevent_ididempotency_keyamountstatusCreated at checkout. idempotency_key is unique so a duplicate booking request maps to the same order instead of creating a second. status tracks pending, paid, failed, refunded.
order_seatsorder_idseat_idevent_idunit_priceThe join that binds sold seats to an order. A unique constraint on (event_id, seat_id) here is a second line of defense: even a buggy service cannot attach one seat to two paid orders.
queue_tokenstoken_idevent_iduser_idpositionadmitted_atexpires_atWaiting-room state. position drives the serving counter, admitted_at marks when the access token was granted, and expires_at bounds how long an admitted fan can occupy the purchase flow.
Deep dives
These are the conversations the interviewer is steering you toward. Practice each one until you can talk through it without notes.
Preventing double-selling: the atomic held-to-sold transition
The one invariant that cannot bend is that a seat is sold once. Two designs work. Pessimistic: inside a database transaction, SELECT the seat row FOR UPDATE, verify it is still available or held-by-this-user, update it to sold, then commit. The row lock forces competing transactions to wait and then see the new state, so the second one fails cleanly. This is simple and correct, and it is fine precisely because the waiting room has already capped how many commits arrive per second. Optimistic: read the seat with a version number, then UPDATE ... WHERE seat_id = ? AND version = ? AND state = 'held'; if zero rows change, someone beat you and you retry or fail. Optimistic scales better under moderate contention and avoids held locks, but on a single ultra-hot seat it can thrash with retries. In practice you combine them: hold the seat first (removing most contention from the commit), then do a short pessimistic or conditional commit. The unique constraint on order_seats is the backstop that makes a double-sell impossible even if application logic has a bug.
The virtual waiting room as a load shedder
You never let a million concurrent buyers reach the booking database, because no single-writer inventory store survives that. The waiting room turns a stampede into a paced line. When an event is under queue control, the edge parks arrivals, assigns each a token, and advances a serving counter at a rate derived from measured backend capacity, not from demand. Fans poll a cheap status endpoint or receive pushes, and only when their number is served do they receive a signed, short-lived access token that the booking API will accept. Everything else is rejected, which also blunts scripts that try to skip the line. AWS and Queue-it describe exactly this pattern, batching bursts through a queue (SQS in the AWS reference) and admitting users as capacity frees up. The key insight for an interview is that the waiting room converts an availability-and-correctness problem into a throughput-shaping problem: the hard concurrency is absorbed by a stateless, horizontally scalable front tier, and the stateful commit path only ever sees a trickle.
Temporary holds with automatic expiry
Selecting a seat should not require finishing payment to protect it, otherwise two fans race through checkout on the same seat. So selection places a hold: the seat flips to held and a hold record is written with an expiry a few minutes out. The natural home for the hot copy is Redis, using a key per seat with a TTL, set with an atomic operation like SET NX so only the first selector wins and the rest see the seat as taken. When the TTL elapses, the hold vanishes and the seat is available again, which means an abandoned cart cleans up itself without a human. There is a subtlety: TTL expiry in the cache must be reflected in the store of record and the seat map. Relying on Redis keyspace notifications alone is fragile, so you also run a sweeper or push a message onto a delayed queue at hold time that fires at expiry and reconciles state. The hold window is a product lever. Shorter windows raise effective inventory turnover during a sellout because stranded seats return faster, at the cost of rushing genuine buyers.
Idempotency on the booking call
Networks retry. A fan double-clicks, a mobile connection drops mid-request and the client resends, a proxy replays. Without protection, one intent becomes two orders and two charges. The fix is an idempotency key: the client generates a unique key for the checkout attempt and sends it on every retry. The booking service stores the key with a unique constraint and, on a duplicate, returns the result of the first attempt instead of executing again. This has to wrap the whole unit, seat commit plus payment intent, so a retry cannot commit the seat a second time or create a second charge. Payment providers like Stripe accept their own idempotency keys, which you thread through so the charge is also deduplicated end to end. Done right, the booking call becomes safely retryable, which is what lets clients and gateways retry aggressively during the chaos of an on-sale without corrupting inventory.
Serving the seat map under massive read fan-out
During an on-sale, reads outnumber writes by orders of magnitude because every waiting fan stares at the map and refreshes. If those reads hit the transactional database they compete with the commits that actually matter. So the map is served from a read-optimized path: a cache holds current seat states, and changes are pushed to connected clients over WebSocket or SSE so seats gray out as others grab them. This introduces a deliberate, tiny window of staleness. A fan may click a seat that just got held a few hundred milliseconds ago, which is fine, because the authoritative check happens at hold time. The cache is optimistic and fast, the reservation service is the arbiter. This separation, an eventually consistent view for browsing and a strongly consistent gate at the moment of commitment, is the standard way to keep a hot inventory page snappy without endangering correctness.
Bot defense and fairness
Scarce inventory attracts automation. If bots can hammer the API faster than humans, real fans never get in, and the tickets end up on resale markets. Defenses stack. The waiting room itself helps, because a signed access token gates the purchase flow and unauthenticated API calls are refused. On top of that: rate limiting per account and per IP, device fingerprinting, CAPTCHA or proof-of-work challenges at suspicious points, and account or presale-code eligibility checks so verified fans are prioritized. Randomizing queue position, which Ticketmaster and others describe, removes the incentive to slam the join button at the exact millisecond of on-sale, because arriving a moment early gives no advantage. None of this is perfect, and it is an arms race, but the combination raises the cost of scripted buying enough to shift the odds back toward humans. In an interview, naming both the technical controls and the fairness policy (randomized position, per-account limits) shows you understand the real problem is not only load, it is who gets the seats.
Payment coordination and the failure paths
Money and inventory have to move together, but they live in different systems, so you cannot wrap them in one database transaction. The pattern is: hold the seats, create a pending order, initiate payment, and only finalize the seats to sold on a verified payment success (usually a webhook, never a client claim). If payment fails, times out, or the fan abandons, the hold expires and the seats return. The awkward middle case is a payment that succeeds after the hold already expired and someone else took the seat. You avoid it by making the hold TTL comfortably longer than the payment step, and by re-validating the hold at the moment of commit: the sell only proceeds if this user still owns a live hold on those exact seats. If not, you refund immediately and apologize, treating it as a rare compensating action rather than the normal path. This is a saga in miniature, with the hold as the reservation stage and payment confirmation as the trigger to commit or compensate.
Trade-offs to discuss
Every senior interviewer expects you to surface at least 3 of these. Pick the decisions, state the alternatives, and justify your choice.
Virtual waiting room versus letting all traffic hit the booking backend
A queue adds a layer and a little wait, but it is the difference between a controlled sellout and a crashed site. The alternative, autoscaling the transactional tier to a million concurrent writers, does not exist for a single-inventory store. Shed and pace the load.
Pessimistic locking versus optimistic concurrency on the sell
Pessimistic (SELECT FOR UPDATE) is dead simple and correct, and it is affordable because the waiting room already limited commit rate. Optimistic scales better and avoids held locks, but retries thrash on a single ultra-hot seat. Holding the seat first removes most contention, so a short pessimistic commit is usually the pragmatic choice.
Hold in Redis with TTL versus holds only in the relational database
Redis gives atomic SET NX and automatic expiry at very high throughput, ideal for the hot selection path. A database-only hold is simpler and has one source of truth, but expiry needs a sweeper and the write rate is higher. Most designs use Redis for speed plus the database for durability and reconcile between them.
Short hold window versus long hold window
A short TTL returns abandoned seats to inventory faster, which matters during a sellout, but rushes real buyers and raises checkout errors. A long TTL is friendlier but strands inventory and can let a payment complete after someone else grabbed the freed seat. Pick a window slightly longer than a realistic payment step.
Strong consistency at commit versus eventual consistency everywhere
Browsing and the seat map can be eventually consistent and cached, which keeps them fast under huge read load. The held-to-sold transition cannot: it needs a strongly consistent, serialized gate. Splitting the system this way gives you both speed for reads and correctness where it counts.
Randomized queue position versus strict first-come-first-served
Strict FCFS feels fair but rewards the fastest scripts and punishes fans with slow connections, and it concentrates a thundering herd at the exact on-sale instant. Randomizing position among everyone who joined the pre-queue removes the millisecond race and blunts bots, at the cost of the intuitive but exploitable earliest-click-wins model.
How Ticketmaster actually does it
Ticketmaster's public material describes Smart Queue as a virtual waiting room that signs fans in, assigns queue positions when a sale starts, then admits them at a controlled pace to minimize checkout errors and maximize sell-through, alongside bot protection and an interactive seat map. The clearest engineering write-ups of this pattern come from adjacent players and cloud vendors. SeatGeek documented a virtual waiting room on AWS built from an API Gateway plus Lambda "Bouncer" and a token service backed by DynamoDB, where the number of access tokens issued before a sale is tied to available inventory and tokens are handed out FIFO or by customer status. AWS publishes a reference Virtual Waiting Room solution that batches request bursts through SQS, tracks a serving position, and issues a signed, time-limited JWT that downstream protected APIs require, refusing anyone without a valid token. Queue-it, which powers waiting rooms for many ticketing and retail flash sales, describes the same shape with DynamoDB or similar as the low-latency queue-state store. The reservation-with-hold plus idempotent-commit core is standard across booking systems, and the double-booking prevention rests on well-understood database locking, optimistic versus pessimistic, that any concurrency reference covers.
Lessons to study before this interview
If any of these topics are fuzzy, the interviewer will catch it. Each lesson is 15 to 60 minutes with diagrams, code, and a quiz.
Related system design interview questions
Practice these next. They lean on the same core building blocks as Ticketmaster.