Design Ticketmaster: the system design interview walkthrough
The request to design Ticketmaster sounds like one line — users browse events, pick seats, and pay — and that one line hides the whole interview. Booking a single seat for a single user is one database row update; design a ticket booking system that sells a finite set of seats to a flash-crowd of millions the instant tickets drop — without ever selling the same seat twice or overselling — and it becomes a concurrency problem with strong-consistency demands under brutal contention. This is one of the most common ticket booking system design prompts because it forces the two hardest muscles at once: correctness under a race, and survival under a stampede.
The round turns on one structural insight. The entire problem is stopping two people from buying the same seat. You do not do that by "saving the booking" — you do it with a seat reservation: tapping a seat creates a short-lived hold so no one else can take it while the buyer pays, and the seat inventory sits behind strong consistency so a race can only ever have one winner. Get that one reframe — the seat is a tiny contended resource, not a row you casually write — and the rest of the system design interview is deciding how to enforce exclusivity (locking), how to not fall over when millions arrive together (the thundering herd), and how to release a seat cleanly when a buyer walks away mid-checkout. This walkthrough follows the path a strong candidate draws on the whiteboard — requirements, estimate, API and data model, high-level design, then the deep dives — and at each step it names the follow-up the interviewer uses to tell a memorised answer from an engineer who understands the tradeoff.
How the ticket booking round is actually run
A ticket booking round runs about 60 minutes on a shared canvas — you draw the boxes and arrows while you talk, and the interviewer interrupts. It is a favourite because the one-line problem opens onto deep water fast: the moment you draw "user picks a seat, user pays," the question becomes what happens when two users pick the same seat, and every answer after that (locking, waiting rooms, expiry) compounds from that single race. Get the hold-and-lock spine clean and you have shown the whole loop in one problem.
The interviewer rarely lets "the user selects a seat and checks out" stand. "Two users tap the same seat in the same millisecond, both hit checkout — walk me through exactly what stops them both getting it, and what the other one sees." That question — correctness under a race — is the one a copied answer can never rehearse, and the one this round is really testing.
The rounds here are modelled on how the loop runs, not scraped from any candidate's recall. What matters for prep is the shape: a blank canvas, a clock, and a follow-up after every decision you make.
The five-step framework for any HLD round
Every high-level design answer fits the same spine. Say it out loud at the start so the interviewer knows you have a plan, then walk it:
- Clarify requirements — functional and non-functional, and what you are explicitly not building.
- Estimate — concurrent users at on-sale, seats per event, read:write ratio and hold duration, to justify every later choice.
- High-level design — the boxes and arrows: clients, waiting room, browse path, booking core, payment.
- Deep dive — the two or three decisions that actually decide this system.
- Follow-ups — bottlenecks, failure modes, and what you would change at ten times the scale.
What the debrief measures
The score is not "right or wrong design." A good HLD debrief reads on four dimensions, and every move below maps to one: problem solving (did you reach a hold-plus-lock on a strongly-consistent seat inventory that cannot double-book), communication (did you say why a seat is a contended resource and not a casual write in a sentence), technical execution (SELECT ... FOR UPDATE versus optimistic version checks, a virtual waiting room, idempotent payment, an expiry sweeper), and time management (did you spend your 60 minutes on the hold, the thundering herd, and expiry, not on the seat-map UI). Reading a walkthrough teaches the design; it does not teach you to deliver it on the clock while someone pushes back.
Step 1 — Requirements: what a ticket booking system must do
Spend the first few minutes here. Jumping to boxes before you have scoped the problem is the fastest way to design the wrong system.
| Type | Requirement |
|---|---|
| Functional | Users browse events and view a live seat map; users hold specific seats for a short window while they pay; users confirm a booking by paying; a seat is sold to at most one user; abandoned holds return to the pool automatically. |
| Functional (optional) | Best-available auto-assignment, dynamic pricing, waitlists, resale/transfer — call these out, then scope most of them for later. |
| Non-functional | No double-booking ever (correctness is non-negotiable); strong consistency on seat inventory; survive huge on-sale spikes; low-latency browsing; high availability for reads even while the write path is protected. |
The sentence that earns points: "The hard part isn't the booking record, it's that one seat is a tiny contended resource a million people may reach for at once — so the core of the design is a short-lived hold on a strongly-consistent seat inventory, not a casual row write." Say that and you have framed the entire design before drawing a box.
Step 2 — Capacity estimation (back of the envelope)
Estimation here is about proving one thing: the hard axis is concurrency and contention on a small, finite inventory, not raw data volume.
| Quantity | Assumption | Result |
|---|---|---|
| Inventory per event | tens of thousands of seats | tiny, finite data — so strong consistency is cheap to afford, not a scale problem |
| Peak concurrent users at on-sale | millions arriving in the first minute | a thundering herd — needs a waiting room and per-user rate limiting |
| Read:write ratio | browsing and seat-map reads vastly outnumber bookings | cache the read path; protect the narrow write path |
| Hold duration | a few minutes per hold | holds must expire and return seats, or inventory silently leaks |
Two conclusions fall out, and you should say both out loud. First, the data is tiny but the contention is enormous — a sold-out arena is tens of thousands of rows, so you can afford strong consistency on inventory; the problem was never storage, it is thousands of requests colliding on the same seat. Second, because reads dwarf writes, you split the system: serve the read-heavy browse and seat-map path from caches and replicas, and keep only the actual hold-and-book on a small, strongly-consistent transactional path. You have now pointed the interview at the real deep dives — locking and the thundering herd — before anyone asked.
Step 3 — API and data model
The API is deliberately small: browse reads, a hold that returns an expiry, and a confirm that turns a paid hold into a booking.
| Operation | Shape |
|---|---|
search_events(query) |
returns matching events; a pure cacheable read. |
get_seat_map(event) |
returns seats and their current state for an event; read-heavy, served from cache/replicas. |
hold_seats(event, seats, user) |
attempts to reserve the seats; returns a hold with an expires_at, or fails if any seat is already taken. |
confirm_booking(hold, payment) |
charges payment and, on success, flips the held seats to booked and issues tickets. |
release(hold) |
explicitly frees a hold's seats back to available (also triggered by expiry). |
The data model is thin, and the seat carries the state and the concurrency control:
| Entity | Notes |
|---|---|
event |
event_id, venue, time, status; mostly static, heavily cached. |
seat |
seat_id, event_id, state (available / held / booked), version — the contended row; version powers optimistic concurrency. |
hold |
hold_id, seat_ids, user_id, expires_at — the short-lived reservation that blocks everyone else. |
booking |
booking_id, user_id, seat_ids, confirmed timestamp — created only after payment succeeds. |
payment |
payment_id, hold_id, amount, status, idempotency_key — the key makes a retried charge safe. |
Step 4 — High-level design: the read path and the booking core
Now the boxes. Clients hit a virtual waiting room / queue that admits them into the purchase flow at a controlled rate, then an API gateway. Behind it the system splits in two:
- Browse / search service (the read path): serves event search and the seat map from caches and read replicas. This path is read-heavy and can be eventually consistent — a seat map that is a second stale is fine, because the hold step re-checks the truth.
- Booking service (the write path): the small, strongly-consistent core. It takes holds and confirms bookings against a single source of truth per event — the seat inventory — using a transaction and a lock so a seat can only be taken once.
- Payment service: talks to an external payment gateway, keyed by an idempotency key so a retried charge never double-charges.
- Ticketing / notification service: issues the ticket and notifies the user once a booking is confirmed.
-
Hold-expiry sweeper: a background job (or a TTL on the hold) that returns abandoned holds to
availableso inventory is never leaked.
Say explicitly that the design is two paths, not one — a large read-heavy, cacheable, eventually-consistent browse path, and a tiny strongly-consistent write path that holds and books one seat. The entire architecture exists to protect that narrow transactional core from the flood. Naming that split unprompted is a signal the interviewer is listening for.
Deep dive: preventing double-booking with holds and locking
This is the first decision that actually decides the round. Two users reach for the same seat — exactly what stops them both getting it?
A seat moves through three states, and the middle one is the whole trick: available → held → booked. Selecting a seat does not book it; it creates a temporary hold with a short TTL (a few minutes) so no one else can take the seat while the buyer pays. Confirming payment flips held → booked; abandoning it returns the seat to available.
| Seat state | Meaning | Who can act |
|---|---|---|
available |
nobody holds it | any user may hold it |
held |
reserved by one user, with an expires_at
|
only the holder may confirm; others are blocked until expiry |
booked |
paid for and issued | nobody — it is sold |
Exclusivity on the available → held transition is the hard part, and there are two standard ways to enforce it:
| Approach | How it works | Cost |
|---|---|---|
| Pessimistic locking | Inside a transaction, SELECT ... FOR UPDATE the seat row so it is locked; check it is still available, set it to held, commit. A concurrent attempt blocks until the lock releases, then sees held and fails. |
Simple and correct; locks held during the transaction hurt throughput under heavy contention on a hot seat. |
| Optimistic concurrency | Read the seat with its version; on write, compare-and-set WHERE version = :read_version; if another writer won, your update affects zero rows — you retry or report the seat taken. |
No locks, great when conflicts are rare; under a true stampede on one seat, retries pile up and most callers lose. |
Either way, the seat inventory must be a single strongly-consistent source of truth per event — never eventually-consistent — because the one guarantee you cannot break is "at most one winner." The read path can be stale; the hold transition cannot.
"Two users tap the same seat in the same millisecond — exactly what stops them both getting it?" The
available → heldtransition runs inside a transaction on the one seat row: withSELECT ... FOR UPDATEthe second writer blocks, then seesheldand is told the seat is gone; with a version check, the second writer's compare-and-set matches zero rows and fails. Either way there is exactly one winner, and the loser is re-shown the seat map.
System Design
HLD · Payments & marketplaces · 60 min
Design Ticketmaster out loud, with follow-ups
Deep dive: surviving the thundering herd
Locking decides correctness for one seat; this decides whether the whole system stays up when a hot event drops and millions arrive in the same minute. The follow-up is always the stampede.
The problem: a huge on-sale might bring two million users for fifty thousand seats in sixty seconds. If every one of them hammers the booking database at once, the strongly-consistent core collapses — and the moment it collapses, your correctness guarantee goes with it. The answer is to never let the flood reach the write path:
- Virtual waiting room / queue. Arrivals are placed in a queue and admitted into the purchase flow at a controlled rate the booking core can actually sustain. Users see a position and an estimated wait instead of a timeout. The admission rate — not the crowd size — is what the booking database ever sees.
- Rate-limit per user. Cap requests per user (and per IP) so a single client or bot cannot flood the hold endpoint; this also blunts scalping scripts.
- Serve the read path from caches and replicas. Browsing, event pages, and the seat map are read-heavy and tolerate slight staleness, so they come from caches and read replicas and scale horizontally — completely off the transactional path.
- Keep only hold-and-book on the strongly-consistent path. The narrow write path handles a trickle of admitted users, not the whole crowd, so its locks and transactions stay fast.
The one-sentence version: you protect the small consistent core by admitting load, not absorbing it — the waiting room shapes two million arrivals into a rate the booking database can serve correctly, while the read path soaks up everything that does not need consistency.
"A huge on-sale: two million users, fifty thousand seats, all in one minute — how do you not fall over?" You do not send two million requests at the seat inventory. A virtual waiting room queues arrivals and admits them at a rate the booking core can sustain; per-user rate limits stop any one client flooding; and everything read-only — search, the seat map — is served from caches and replicas. The strongly-consistent write path only ever sees the admitted trickle, so it keeps its correctness instead of melting.
Deep dive: payment, consistency and releasing expired holds
Locking decides who wins the seat and the waiting room decides who gets to try; this decides what happens between "held" and "booked" — the flow that spans the inventory and payment services, and the seat that must come back when a buyer walks away.
The hold → pay → confirm flow crosses two services, so you cannot treat it as one atomic write. The honest answer is a saga: a sequence of steps across inventory and payment, each with a compensating action if a later step fails.
| Outcome | What happens to payment | What happens to the seat |
|---|---|---|
| Paid | charge succeeds (idempotency key ensures a retry does not double-charge) |
held → booked; ticket issued |
| Failed | charge declines or errors | hold is released; seat returns to available (compensate) |
| Abandoned | no charge; the buyer closes the tab | the hold's expires_at passes; the sweeper (or TTL) returns the seat to available
|
Two mechanisms make this safe. Idempotent payment: every charge carries an idempotency key, so if the client retries a timed-out request the gateway recognises the key and does not charge twice — essential when you cannot tell whether a dropped response meant success or failure. An expiry sweeper: because a buyer can simply disappear mid-checkout, every hold has an expires_at; a background sweeper (or a TTL on the hold) flips expired holds back to available. Without it, abandoned holds would quietly lock seats out of sale forever — inventory leakage that looks like a sold-out event that is actually half empty.
"A user holds a seat then closes the tab mid-checkout — when and how does that seat come back?" The hold was written with an
expires_ata few minutes out. When that passes with no confirmed payment, the expiry sweeper (or the hold's TTL) flips the seatheld → available, so it is offered again. Nothing waits on the vanished user, and because payment is keyed idempotently, even a late retry of their charge can't resurrect a released hold into a double-booking.
System Design
HLD · Payments & marketplaces · 60 min
Defend payment, consistency and expiry out loud
The follow-ups that decide the ticket booking round
Scan the probes from each deep dive and they rhyme. Master these five and you are ready for a version of the question that is not drawn exactly like this one.
-
"Two users tap the same seat at once — what stops both getting it?" → The
available → heldtransition runs in a transaction on the one seat row, enforced bySELECT ... FOR UPDATEor a version compare-and-set; exactly one winner, the loser is re-shown the map. (problem solving) - "Pessimistic or optimistic locking — which, and why?" → Pessimistic is simplest and safe but blocks under contention on a hot seat; optimistic avoids locks but retries pile up in a stampede. Name the trade; both need a strongly-consistent inventory. (technical execution)
- "Two million users, one minute — how do you not fall over?" → A virtual waiting room admits users at a sustainable rate, per-user rate limits stop flooding, and the read path is cached; the write path only sees the admitted trickle. (problem solving)
-
"A buyer abandons checkout — when does the seat come back?" → Every hold has an
expires_at; a sweeper or TTL flips expired holds back toavailable, so nothing waits on a vanished user and inventory never leaks. (technical execution) - "A payment times out — did it go through, and will you double-charge?" → An idempotency key makes the retry safe; a saga confirms the booking on success and compensates (releases the hold) on failure. (communication)
Each maps to a debrief dimension. Reading this list teaches you the answers; it does not teach you to deliver them while someone interrupts — that only comes from saying them out loud.
How to prepare: a one-week plan
You do not need fifty systems. You need this one cold and the framework automatic, so the next "design X" is just new nouns in the same five steps.
- Days 1–2: Learn the five-step framework until it is reflex. Do the ticket booking system end to end on paper — requirements, estimate, API, data model, the read-path/booking-core boxes.
-
Days 3–4: Drill the locking deep dive: the available → held → booked states,
SELECT ... FOR UPDATEversus optimistic version checks, and why inventory must be strongly consistent. Say each tradeoff in one sentence. - Days 5–6: Own the thundering herd and the payment flow — virtual waiting room, per-user rate limiting, read/write split, idempotent payment, saga compensation, and the expiry sweeper. Answer each of the five follow-ups out loud.
- Day 7: A full timed 60-minute round, narrating the whole way, with someone (or something) interrupting. This is the rep that counts.
This post is the deep walkthrough behind one row of the broader Top 25 system design interview questions — once the ticket booking system is automatic, the same spine carries the rest of the list.
Frequently asked questions
How do I design Ticketmaster step by step?
Follow the five-step HLD framework. Clarify requirements (browse events, hold specific seats for a short window, confirm by paying, never double-book, release abandoned holds); estimate to show the hard axis is concurrency on a small finite inventory, not data volume; define the small API (search, get seat map, hold seats returning an expiry, confirm booking, release) with a thin event/seat/hold/booking/payment model; draw the design as two paths (a cached read path for browse and seat maps behind a virtual waiting room, and a strongly-consistent booking core for holds and bookings, plus payment and a hold-expiry sweeper); then deep-dive the decisions that matter — holds and locking, the thundering herd, and payment with expiry. Finish on the follow-ups about races and abandoned checkouts.
How does a ticket booking system stop double-booking?
A seat moves available → held → booked, and the middle state is the trick: selecting a seat creates a short-lived hold so nobody else can take it while the buyer pays. The available → held transition runs inside a transaction on the single seat row, enforced either by pessimistic locking (SELECT ... FOR UPDATE, so a concurrent writer blocks then sees the seat is taken) or by optimistic concurrency (a version column with compare-and-set, so a losing writer's update affects zero rows and retries or fails). The seat inventory is a strongly-consistent single source of truth per event, so a race can only ever have one winner.
What is a seat hold and why does it expire?
A seat hold is a temporary reservation created the moment a user selects a seat: it marks the seat held, records who holds it, and stamps an expires_at a few minutes out, blocking everyone else while the buyer completes payment. It expires because buyers abandon checkouts — they close the tab, lose signal, or change their mind — and if the seat stayed held forever it would be locked out of sale permanently. A background expiry sweeper (or a TTL on the hold) flips expired holds back to available, so abandoned seats return to the pool and inventory never silently leaks.
Should I use pessimistic or optimistic locking for seats?
Both guarantee at most one winner on a seat; they trade differently under load. Pessimistic locking (SELECT ... FOR UPDATE inside a transaction) is the simplest to reason about and is safe, but it holds a lock for the duration of the transaction, so heavy contention on one hot seat serialises callers and hurts throughput. Optimistic concurrency (a version column and compare-and-set) takes no locks and shines when conflicts are rare, but in a true stampede on a single seat the retries pile up and almost everyone loses. The interview-strong answer names the trade and keeps the inventory strongly consistent either way.
How does a ticket booking system survive a huge on-sale spike?
You protect the small strongly-consistent core by admitting load rather than absorbing it. A virtual waiting room queues the millions of arrivals and admits them into the purchase flow at a rate the booking database can actually sustain, so the write path only ever sees a trickle. Per-user (and per-IP) rate limiting stops any single client or bot from flooding the hold endpoint. Everything read-only — event search and the seat map — is served from caches and read replicas that scale horizontally, completely off the transactional path, so browsing stays fast while the hold-and-book core keeps its correctness.
How do I practise a system design interview out loud?
Reading a walkthrough is not the same as defending a design while someone interrupts. On Zynter, the HLD track runs live voice rounds modelled on how real system design loops run — you talk through a shared design on the canvas, and a voice interviewer pushes with the follow-ups above one at a time, then gives you a debrief on problem solving, communication, technical execution, and time management. The score is a practice signal, not a hiring decision.
Practise it on Zynter
You can read every seat state. The round is won on the race and the stampede.
Pick the Ticketmaster round, draw the read-path and booking-core design on the canvas, and defend it out loud to a voice interviewer that keeps asking "two users, same seat, same millisecond — who wins?" and "two million arrivals in a minute — how do you not fall over?" — then read the debrief. Zynter.ai is on-demand mock interviews modelled on real company loops, with 150+ rounds across 20 companies including the HLD system design track (Sep 2026).
Practise the Ticketmaster round →
Browse all interview tracks




Top comments (0)