A big show goes on sale. In the first minute, hundreds of thousands of people all want the same few good seats. Two people cannot buy seat 12A. The system has to pick a winner, tell the loser instantly, and do it while absorbing a traffic spike that dwarfs its normal load. This is one of the cleanest examples of a concurrency problem in the wild, and the solution is a careful dance between locking, timeouts, and queues.
The core problem
A seat is a single unit of inventory that exactly one person can own. The moment you have shared mutable inventory and many concurrent buyers, you have a race. Two requests read "seat available", both proceed to buy, both succeed, and now two people are holding tickets to the same chair. Every wrong design of this system fails in exactly that spot: the gap between checking availability and claiming it.
The extra twist is that buying a ticket is not instant. A user selects seats, then spends a few minutes entering payment. During those minutes the seat cannot be sold to anyone else, but it also cannot be locked forever if they wander off.
Key design decisions
Reserve first, pay second, with a time-limited hold. When a user selects a seat, you place a temporary hold that reserves it for a few minutes. If they complete payment in time, the hold becomes a purchase. If the timer expires, the hold is released and the seat returns to the pool. This turns a long, risky transaction into two short ones.
Make the reservation atomic. The hold must be an atomic claim, so that only one request can transition a seat from available to held. In a relational database this is a conditional update: UPDATE the seat SET status = held WHERE seat_id = X AND status = available. If it affects one row, you won the seat. If it affects zero rows, someone beat you. The database's row lock does the arbitration. No application-level check-then-set, because that is the race.
Expire holds reliably. A hold needs a deadline. You can store an expiry timestamp and let a background sweeper release stale holds, or use a store with native TTL. Either way, the seat must not be lost forever because someone closed their laptop mid-checkout.
Put a waiting room in front of the sale. During an on-sale spike, you do not let all traffic hit the inventory system at once. A virtual waiting room admits users in controlled batches, often as a queue. This smooths the load into something the backend can handle and gives users a fair, visible position instead of a flurry of failed requests.
Consistency over availability, on purpose
This is a system that chooses correctness over raw availability. It is better to make a user wait, or to briefly show a seat as unavailable, than to sell it twice. That framing (never oversell, even at the cost of some latency) drives most of the design and is worth stating out loud in an interview.
How the real systems do it
Ticketmaster and similar high-demand ticketing systems use exactly this shape: a queue-based waiting room to shape the flood, short-lived seat holds backed by atomic reservations, and a hard timeout that recycles abandoned holds. Inventory lives in a strongly consistent store because the whole point is to never double-allocate. Caching is used heavily for the read path (venue maps, seat layouts, pricing) but the write path that actually claims a seat stays strict and serialized per seat. Payment happens against an already-held seat, so a slow card does not block anyone else from the rest of the venue.
The durable lesson: separate the reservation from the payment, make the reservation an atomic conditional claim, give it a timeout, and shape the incoming traffic with a queue. Double-selling is a concurrency bug, and you beat it by letting the database, not your code, decide who won the seat.
I wrote the full breakdown, with the hold state machine and the queue design, here: https://www.systemdesign.academy/interview/design-ticketmaster
Top comments (0)