"Design Ticketmaster" — a classic ticket booking system design question — is deceptively hard. It looks like a simple CRUD app until you realize the entire problem is contention: thousands of people trying to buy the same handful of seats in the same second, and never selling one seat twice. Below is a structured walkthrough you can mirror in a real system design interview, narrating each stage as you go. Keep the system design interview cheat sheet open alongside for the shared building blocks.
1. Clarify the requirements
Don't start drawing boxes. Scope the problem first so you optimize the right thing.
Functional requirements
- Browse events and view a venue's seat map with real-time availability.
- Select and hold specific seats while checking out.
- Pay for held seats to complete a booking.
- Held seats are released automatically if checkout isn't finished.
Non-functional requirements
- No double-booking: a seat is sold to exactly one buyer — correctness is paramount.
- High concurrency: popular on-sales draw huge simultaneous spikes.
- Consistency on the booking path: inventory must be strongly consistent; browsing can be eventually consistent.
- Availability & low latency for browsing and seat maps.
I'll assume reserved seating (specific seats, not general admission), a single payment provider, and that we can show a "reserved" seat as unavailable to others immediately. Calling out these assumptions shows judgment without burning time.
2. Capacity and the shape of the load
The traffic is bursty and skewed, which drives the whole design. Browsing is read-heavy and steady; the booking path is write-heavy but concentrated into short, intense windows.
| Quantity | Assumption | Result |
|---|---|---|
| Seat-map / browse reads | steady, cacheable | High volume, easy to scale |
| Popular on-sale | ~1M users, ~50K seats | ~20:1 demand-to-supply |
| Peak booking attempts | burst at on-sale open | Tens of thousands/sec, for minutes |
The headline takeaway: demand vastly exceeds supply during flash sales, so the design must both stay correct under contention and shed load gracefully. That points straight to reservations plus a waiting room.
3. API design
A small REST surface covers the flow. The reservation call is the hot, contended path.
GET /v1/events/{id}/seats -> seat map with availability
POST /v1/reservations { "eventId": "...", "seatIds": [...] }
-> holds seats with a TTL, returns reservationId
POST /v1/reservations/{id}/pay { "paymentToken": "..." }
-> confirms booking on payment success
DELETE /v1/reservations/{id} -> release held seats early
Splitting reserve from pay is the key modeling choice: it lets us hold inventory the instant a user picks a seat, then take the slower payment step without blocking anyone else's attempt.
4. Data model and seat states
The heart of the design is a seat's lifecycle. Each seat is a row keyed by (eventId, seatId) with a status that moves through exactly three states.
Seat lifecycle (state machine)
+-------------+ reserve (atomic, sets TTL) +-------------+
| AVAILABLE | -----------------------------> | RESERVED |
+-------------+ +-------------+
^ | |
| TTL expires / user cancels | | pay succeeds
+------------------------------------------+ v
+-------------+
| BOOKED |
+-------------+
AVAILABLE -> RESERVED : conditional update, only one request wins
RESERVED -> BOOKED : only after payment confirmation
RESERVED -> AVAILABLE: TTL lapses or explicit cancel
| Entity | Key fields | Notes |
|---|---|---|
| Event | eventId, venueId, startTime | Cacheable; read-heavy |
| Seat | eventId, seatId, status, reservedBy, reservedUntil | Strongly consistent; sharded by eventId |
| Reservation | reservationId, userId, seatIds, expiresAt, status | Drives the TTL and payment step |
| Booking | bookingId, reservationId, paymentId, createdAt | Immutable record of a completed sale |
5. The core problem: concurrency and overselling
This is where the interview is won or lost. Two people click the same seat at the same instant — how do we guarantee only one succeeds and the seat is never sold twice?
Option A: pessimistic database lock. Lock the seat row for the whole checkout. It's correct, but it ties up a connection for minutes, doesn't survive a user abandoning the flow or a client crash, and scales badly under a flash-sale burst. Mention it, then reject it.
Option B: reservation with a TTL (the strong answer). Reserving a seat is a single atomic conditional update: flip the seat from AVAILABLE to RESERVED only if it's currently available, stamping a reservedUntil a few minutes out. Databases serialize this — exactly one concurrent request wins; the rest get "seat taken." The hold is bounded by the TTL, so an abandoned checkout self-heals when the TTL lapses and a background sweeper (or a lazy check on read) returns the seat to AVAILABLE. No long-lived locks, no manual cleanup.
| Pessimistic lock | Reservation + TTL | |
|---|---|---|
| Correctness | Yes | Yes (atomic conditional update) |
| Holds resources | Whole checkout (a connection) | Just a status + timestamp |
| Abandoned checkout | Needs timeout / cleanup | Self-heals on TTL expiry |
| Scales under bursts | Poorly | Well |
Landing on reservation-with-TTL, and explaining that the atomic conditional update is what actually prevents overselling, is the single highest-signal moment in this interview. Because correctness beats speed here, the seat-inventory store favors consistency over availability.
6. The payment step
Payment is slow and external, so it must never hold the inventory lock. The flow: the seat is already RESERVED with a TTL, so the user has a private window to pay. On payment success, transition the seat RESERVED → BOOKED and write an immutable booking record. On failure or timeout, do nothing — the TTL lapses and the seat frees itself.
Two details signal seniority. First, integrate the payment provider through webhooks and treat confirmation idempotently (a duplicate callback must not create two bookings) — the same discipline covered in designing a payment system. Second, make sure the TTL comfortably exceeds realistic payment latency so users aren't losing seats mid-transaction.
7. Flash-sale spikes and queueing
When a blockbuster on-sale opens, millions arrive in the same minute for tens of thousands of seats. Letting all of them slam the inventory database at once causes lock contention, timeouts, and a poor experience. The fix is a virtual waiting room.
Millions of users
|
v
+----------------+ admit in batches sized to capacity
| Waiting room / | ------------------------------------------> Purchase flow
| queue (token) | |
+----------------+ v
| hold most users +-----------------------+
v | Reservation service |
"You are #48,210" | (atomic seat holds) |
+-----------------------+
|
v
[ Seat inventory DB ]
(strongly consistent)
Users get a queue position and a token; the system releases them into the purchase flow in batches matched to remaining capacity. This smooths the spike, shields the strongly consistent inventory tier, and gives honest feedback instead of errors. Rate-limiting admission is closely related to how you'd build a rate limiter, and the queue itself often leans on a fast in-memory store like a distributed cache.
8. Architecture and trade-offs
- Split read and write paths. Seat maps and event browsing are cached and served cheaply; the reservation/booking path runs against a strongly consistent inventory store.
- Reservation + TTL over locks. Always land here: atomic conditional updates prevent overselling, and the TTL makes abandoned checkouts self-healing.
- Consistency where it counts. Inventory is strongly consistent to guarantee no double-booking; browsing tolerates eventual consistency for scale.
- Queue to shed load. A waiting room converts an uncontrollable spike into a steady, capacity-matched stream.
- Idempotent payments. Confirm bookings through idempotent webhook handling so retries never double-charge or double-book.
Framework reminder: Design Ticketmaster follows the same arc as every system design answer — requirements → capacity → API → data model → the core decision (concurrency) → trade-offs. The reservation-with-TTL and queueing patterns generalize to any booking system, and the idempotency thread runs straight through designing a payment system.
Practice booking-system design with live AI support
CoPilot Interview surfaces a structured design skeleton — requirements, seat states, the reservation/TTL concurrency answer, and the flash-sale queue — in about 4 seconds during real Zoom and Teams calls. Free tier for Windows and macOS. Explore the free AI interview assistant.
Download freeFAQ
How do you prevent two people from booking the same seat?
By making the seat-reservation step atomic. When a user selects a seat, the system conditionally flips that seat from AVAILABLE to RESERVED in a single atomic operation - only one concurrent request can succeed. The reservation carries a short TTL so the seat is held during checkout and automatically released if payment isn't completed in time.
Why use reservation with a TTL instead of a database lock?
A long-held database lock ties up a connection for the entire checkout and doesn't survive a user abandoning the flow or a client crash. A reservation with a TTL holds the seat for a bounded window (for example five minutes), then expires automatically, freeing the seat without any manual cleanup. It gives the same exclusivity with far better resource usage and self-healing.
How does Ticketmaster handle flash-sale traffic spikes?
With a virtual waiting room or queue. When millions of users hit a popular on-sale at once, the system admits them into the purchase flow at a controlled rate instead of letting all requests slam the inventory database. Users wait in a queue and are released in batches sized to available capacity, which smooths the spike and prevents overload.
How do you avoid overselling tickets?
Overselling is prevented by keeping seat inventory strongly consistent and only decrementing it through atomic, conditional operations. A seat can only move AVAILABLE to RESERVED once, and only RESERVED to BOOKED after payment succeeds. Because correctness matters more than raw speed here, the inventory path favors consistency over availability.
What happens if payment fails after a seat is reserved?
The reservation simply expires. The seat is held in a RESERVED state with a TTL during checkout; if payment fails or the user abandons the flow, the TTL lapses and a background process returns the seat to AVAILABLE. Only a successful payment transitions the seat to BOOKED, so failed payments never leave seats stuck.