System Design · step by stepDesign Ticketmaster
Step 1 / 9

Design Ticketmaster (Event Booking) — the walkthrough in full

A written version of the interactive walkthrough above — the same steps, decisions and trade-offs, laid out for reading, reference and search.

The big idea

How does Ticketmaster avoid double-booking?

When tickets for a hot show drop, thousands of people lunge for the same few good seats in the same second. Sell seat 12C to two of them and you’ve created a furious customer and a refund.

Most of this system is easy — browsing events is just a read-heavy catalog. The hard, interesting core is correctness under contention: guaranteeing a seat goes to exactly one buyer, even under a stampede. Everything we build orbits that one guarantee.

How to read this: Each step opens with a real design decision — you make the call before I show you what ships. Watch the diagram grow, and at the end drop payment to see the saga safely undo a half-finished booking. Hit Begin.

Step 1 · The skeleton

Browse events and seats

Before anyone buys, they browse: find a show, pick a venue, see the seat map and what’s open. This part is a classic read-heavy workload.

Design decision: Before buying, people browse events, venues and seat maps. What kind of workload is that, and how do you serve it?

The call: A read-heavy Catalog Service over Seat Inventory; cache aggressively. — Listing events and rendering seat maps scales like any read-heavy site — cache and replicate. Recognizing browsing is the easy 90% lets you concentrate design effort on the hard buying path.

A Catalog Service serves events, venues and seat maps, reading availability from Seat Inventory — the store that tracks each seat’s state (available / held / sold). Browsing can be cached aggressively; it’s the buying that’s hard.

Reads are the easy 90%: Listing events and rendering seat maps scales like any read-heavy site — cache it, replicate it, done. Recognizing that lets you spend your design effort where it actually matters: the write path.

Step 2 · The hard part

The race condition

Two buyers both see seat 12C as available. Both click "buy." Both requests read available, both write sold. Now it’s sold twice. This is a textbook race condition.

Design decision: Two buyers both see 12C as available and both click buy — both read "available," both write "sold." What is this, and the core fix?

The call: A race condition — make "claim this seat" an operation only one request can win. — Read-then-write in two unguarded steps under concurrency is the textbook race. Enforce a single winner — a short-lived hold, then an atomic transaction at purchase. Naming the race is half the answer.

The fix is to make "claim this seat" an operation only one request can win. You cannot let read-then-write happen in two unguarded steps under concurrency. The next steps are two ways to enforce that single winner: a short-lived hold, then a transaction at purchase.

Name the enemy: concurrency: Almost every booking-system question is really testing whether you can prevent two writers from both "winning" the same resource. Spotting the race — and saying so out loud — is half the answer.

Step 3 · Holds & locks

Reserve before you pay

Checkout takes a couple of minutes — entering card details, confirming. You can’t hold a seat open for everyone that whole time, but you also can’t sell it out from under someone mid-payment.

Design decision: Checkout takes minutes. You can’t hold a seat for everyone that long, but can’t sell it from under someone mid-payment. How?

The call: A short-lived exclusive hold (Redis lock with a TTL) during checkout. — The booking service places an exclusive hold with a 5–10 min TTL: while held no one else can take the seat, completing turns it into a sale, and if the buyer vanishes the TTL auto-expires and the seat returns to the pool.

The Booking Service places a short-lived exclusive hold on the chosen seats — a lock in Redis with a TTL (say 5–10 min). While held, no one else can take them. If the buyer completes, the hold becomes a sale; if they vanish, the TTL auto-expires and the seats return to the pool.

Optimistic vs pessimistic: Pessimistic: lock the seat up front (a Redis hold, or SELECT … FOR UPDATE). Optimistic: let the write proceed and reject on a version mismatch. Hot seats favour pessimistic holds; rarely-contended ones favour optimistic.

Step 4 · Make it correct

A seat is strongly consistent

Holds in a cache are fast, but money is involved — you can’t have the seat’s true state drift or get lost if a node dies. The final claim must be exactly once.

Design decision: Holds live in a fast cache, but money is involved and a node can die. How do you make the FINAL seat claim exactly-once?

The call: Confirm in an ACID transaction: re-check held-by-buyer, flip to sold, atomically. — Treat the seat as strongly consistent: inside one transaction, verify it’s still held by this buyer and flip held→sold atomically. The cache hold gives speed; the transactional write gives the ironclad exactly-once guarantee.

Treat the seat as a strongly consistent resource. Confirm the purchase inside an ACID transaction against the inventory store: re-check the seat is still held by this buyer and flip it to sold atomically. The cache hold gives speed; the transactional write gives the ironclad guarantee.

Pick consistency here, on purpose: Most of the site can be eventually consistent. Seat state cannot — a stale read sells a sold seat. Knowing where to spend strong consistency (and pay its latency cost) is the senior move.

Step 5 · Payment saga

Charge, then confirm — or undo

Payment is a slow call to an external provider that can fail, time out, or succeed-but-not-tell-you. You’re holding seats hostage to its answer, and you must never charge twice.

Design decision: Payment is a slow external call that can fail, time out, or succeed-but-not-tell-you — and you’re holding seats. How do you sequence it?

The call: A saga: hold → charge (idempotent) → confirm on success, release on failure. — Sequence the steps and define a compensating action per failure: on success confirm the sale and emit a receipt; on failure/timeout release the hold so seats return. An idempotency key makes a retried charge never double-bill.

Run checkout as a saga: hold seats → charge the Payment Service → on success, confirm the sale and emit a receipt on the event bus; on failure or timeout, release the hold so the seats come back. Use an idempotency key so a retried charge never double-bills.

Distributed transactions = sagas: You can’t wrap an external card charge and your database in one ACID transaction. Instead, sequence the steps and define a compensating action (release the seats, refund) for each failure — that’s a saga.

Step 6 · The stampede

Tame the on-sale rush

A blockbuster on-sale brings a million people in one minute. Even a perfect booking core melts if all of them hit it simultaneously — and the experience becomes a lottery of errors.

Design decision: A blockbuster on-sale brings a million people in one minute. Even a perfect booking core melts if all hit at once. What do you do?

The call: A virtual waiting room: queue buyers and admit them in controlled waves. — At peak the gateway diverts buyers into a fair queue and admits them to booking in waves sized to capacity. It converts a thundering herd into a steady trickle and makes fairness explicit — admission control, not more servers.

Front the system with a virtual waiting room. At peak, the gateway diverts buyers into a fair queue and admits them to the booking flow in controlled waves, sized to what inventory and the booking service can handle. The crowd waits in an orderly line instead of crashing the door.

Shed and shape load: When demand vastly exceeds capacity, the answer isn’t more servers — it’s admission control. A waiting room converts a thundering herd into a steady, survivable trickle, and makes fairness explicit.

Step 7 · The sharp edges

Scale, fairness & failure

Big venues and many simultaneous events strain one inventory store; bots try to scalp; and holds can leak if a booking node dies mid-flow.

Shard inventory by event so each show’s hot seats live on their own partition. Add bot defenses (CAPTCHAs, per-account limits) and queue fairness to fight scalpers. Run a sweeper that reclaims expired or orphaned holds, so a crashed checkout never permanently strands a seat.

Design for the unhappy path: Crashed checkout → sweeper reclaims the hold. Scalper bot → rate limits and CAPTCHAs. Hot event → its own inventory shard. Handling the failures and abuse is what separates a real ticketing system from a demo.

You did it

You just designed Ticketmaster.

  • A
  • T — h
  • S — h
  • A — n
  • A
  • A
  • S — h
RUN IT YOURSELF

No double-booking: optimistic locking

Selling the last seat to exactly one person is a concurrency problem. Optimistic locking (compare-and-set on a version) solves it. Here it is in both languages, running live. Switch tabs, read the comments, and hit Run.

HOW TO READ THE CODE — 4 IDEAS
  1. Every seat carries a version number alongside its owner.
  2. To book, you pass the version you read earlier; the store commits only if it still matches (step 1).
  3. If someone booked first, the version moved on, so your compare-and-set fails (step 2) — you would retry.
  4. The winner claims the seat and bumps the version (step 3); no two people get the same seat.
CPython · WebAssembly
built to be booked, not memorized — make the calls, drop payment, run the gauntlet.
Finished this one? 0 / 58 System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.