Ticketmaster
Selling each seat exactly once, even when a million people want the same twenty thousand.
What you will learn
- 01The problem
- 02Requirements
- 03Estimation
- 04Data model and the seat state machine
- 05The core problem: two people click the same seat
- 06Holds and expiry
- 07Payment and the booking flow
- 08Surviving the flash sale
- 09Search and discovery
- 10Extra requirements to discuss
- 11What to remember
The problem
An online ticketing service lets people browse events, see a map of seats, choose seats, pay, and receive tickets. Most of the time it is quiet. But when a popular event goes on sale, a million people arrive in the same minute to compete for twenty thousand seats. The central rule is absolute: a seat is never sold twice. Everything else is arranged around protecting that rule under heavy contention.
Requirements
- Browse and search events by place, date and category.
- Show an up-to-date seat map for an event.
- Let a user select seats and hold them briefly while they pay.
- Take payment, and confirm the booking, or release the seats.
- Issue tickets and confirmations.
- Correctness comes first: no double sales and no charge without a ticket.
- Availability matters, but a short delay is better than a wrong sale. Browsing can be eventually consistent. Booking cannot.
Estimation
Assume 3 million tickets sold on a normal day, which averages about 35 per second, trivial. The danger is the spike: one event, 20,000 seats, one million users arriving at once. Even if each user makes a handful of requests, that is millions of requests in a minute, aimed at one small set of rows. So the design is dominated by contention on a hot set of data and by sudden traffic, not by storage volume.
Data model and the seat state machine
The core entities: Event, Venue, Seat (belonging to an event), Booking, User, Payment. Each seat for an event is in one of three states.
- Available: free to choose.
- Held: temporarily reserved for one user, with an expiry time, say 10 minutes.
- Booked: sold, permanently.
Transitions: available to held when a user selects it, held to booked on successful payment, and held back to available when the hold expires, the user leaves, or payment fails. There is no path from booked back to available except an explicit cancellation. A seat is not simply free or sold, and the hold state is what makes checkout possible without double selling.
Swipe sideways to pan · drag a box to move it
Swipe sideways to pan · drag a box to move it
The core problem: two people click the same seat
Check then act again. Two users both read "available", both try to hold it, and both think they succeeded. The fix is to make the claim atomic in the database, so the check and the change are a single step.
A conditional update does it in one statement:
UPDATE seats
SET status = 'held', held_by = :user, hold_expires = :now_plus_10m
WHERE id = :seat
AND (status = 'available' OR (status = 'held' AND hold_expires < :now));
The database counts affected rows. If it is 1, this user got the seat. If it is 0, someone else did, so tell the user it is gone. Only one of two simultaneous updates can match. Other correct options include a row lock (select for update inside a transaction), or optimistic concurrency, where an update succeeds only if a version number is unchanged. A cache such as a key with set-if-absent and an expiry can hold temporary claims, with the database remaining the source of truth.
Because correctness is the goal, the seat data belongs in a relational database with transactions. This is the consistency-over-availability end of the tradeoff: during a failure it is better to refuse a sale than to sell a seat twice.
When a user selects several seats, hold them all in one transaction, so they get all or none, avoiding half-complete holds that block other buyers.
Holds and expiry
Holds must disappear if the user walks away. The hold expiry stored on each seat means a stale hold is treated as available by the condition above, so correctness does not depend on a cleanup job running. A background job still sweeps expired holds, so seat maps show them as free and metrics stay accurate. A countdown in the user interface shows the time left, and the server, not the client, is the authority on whether the hold is still valid.
Payment and the booking flow
- The user holds seats.
- The user pays through an external payment provider, using an idempotency key, so that a retry or double click cannot charge twice.
- On success, a transaction changes the seats from held to booked and creates the booking, only if the hold is still valid and owned by this user.
- On failure or timeout, the seats are released.
The tricky case is payment succeeding after the hold expired, with another user holding or buying the seat. Handle it by checking at confirmation, and if the seat is gone, refund automatically and tell the user clearly. Making the hold slightly longer than the payment timeout reduces this. Record every step, so a crash between charging and booking can be repaired by a reconciliation job comparing payments with bookings.
Swipe sideways to pan · drag a box to move it
Surviving the flash sale
The database can serialize claims on 20,000 seats easily. What it cannot survive is a million users hammering it with requests. Protect it in layers.
- Virtual waiting room: put arriving users in a queue and admit them in controlled batches, for example a few thousand at a time. Everyone gets a place in line, and the booking service sees a steady load. This is the single most effective measure.
- Cache and CDN for reads: event pages and the static seat map layout are the same for everyone, so serve them from a CDN. Seat availability can be cached for a second or two and may be slightly out of date, since the atomic claim is the real check.
- Rate limits and bot defense: limit requests per user and IP, require a challenge for suspicious traffic, and cap tickets per person. Scalpers using automated scripts are a major source of load.
- Shard by event: keep all seats of one event on one shard, so a booking transaction stays local and never spans machines. Different events spread across shards. A single huge event can use a dedicated shard.
- Replicas for browsing: read replicas serve seat maps and search, while writes go to the primary.
Swipe sideways to pan · drag a box to move it
Search and discovery
Event search is a separate, read-heavy concern. Index events by name, location, date and category in a search engine, updated as events change, and keep it apart from the booking database, so heavy searching never slows a sale.
Extra requirements to discuss
- Best available seats: choose a group of adjacent seats automatically, which is a search over the seat map for contiguous free seats in a section.
- Dynamic pricing: prices shift with demand, a separate service that reads demand signals.
- Waitlists and returns: released seats are offered to the waitlist in order.
- Ticket delivery and fraud: issue unique, signed or rotating barcodes, so a screenshot cannot be reused, and verify at the gate.
- Notifications: send confirmations and reminders through a queue, so slow email delivery never delays the booking.
What to remember
- The atomic claim on a seat is the heart of the design, and the hold with expiry makes checkout safe.
- Choose strong consistency for booking and eventual consistency for browsing.
- Protect the core with a waiting room, caching, rate limits and sharding by event.
- Make payment steps idempotent, and reconcile payments with bookings so mistakes can be found and fixed.