Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

System Design

Design Ticketmaster

Step 1 / 9

Learn system design by building an event-ticketing system like Ticketmaster step by step.

The numbers to beat1 seat→ 1 buyer2 writers1 winnernodouble-sell

The whole design, in writing

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

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.

BuyerOther Buyers
New in this step: Buyer, Other Buyers. · swipe to pan the diagram

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.

What the new pieces do

Buyeruser
Someone trying to grab specific seats for a show. Will be racing thousands of others for the same ones.
Other Buyerscrowd
The crowd. The instant tickets drop, they all hit the same handful of good seats at once.

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.

API GatewayCatalog ServiceSeat Inventory
New in this step: API Gateway, Catalog Service, Seat Inventory. · swipe to pan the diagram

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

  1. Browsing is read-heavy and tolerant of slight staleness (a seat map a second old is fine); forcing strong consistency on it wastes effort and latency. Spend that effort on the write path.

  2. 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.

  3. Re-deriving seat state from raw sales per view is slow and needless — keep an authoritative seat-state store and read (and cache) it. The interesting problem is the contended write, not the read.

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.

Why this piece earns its place

The quiet assumption here is that a seat map can be a second or two old and nobody gets hurt. That holds every day except the one that matters. Cache the availability layer the way you cache the event and venue records and, at an on-sale, you hand an enormous crowd a map that all agrees on which seats are open — so they all lunge at the same handful, and the cache converts staleness into contention on exactly the rows you least want contended. So I’d split the caching by how fast a field actually changes: event, venue and seat geometry are nearly static and can be cached for a long time, while the available overlay gets a very short TTL or a push refresh. And I’d make the contract honest all the way up to the UI — availability is a hint, never a promise. The map is allowed to be wrong; the booking path re-checks, and the interface has to be able to say “gone, pick another” without it reading as a bug.

What the new pieces do

API Gatewayedge
Where every request enters. Later it fronts a virtual waiting room to tame the on-sale stampede.
Catalog Serviceservice
Serves the browse experience: events, venues, seat maps and which seats are currently available.
Seat Inventorystore
The source of truth for every seat's state: available, held, or sold. The one thing that must never double-book.

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.

BuyerOther BuyersAPI GatewayCatalog ServiceSeat Inventory
The system as it stands at this step. · swipe to pan the diagram

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

  1. Caching doesn’t cause or fix this — even with fresh reads, two requests can both read "available" then both write "sold." It’s a concurrency problem, not a staleness one.

  2. Speed doesn’t prevent two writers from both winning; the double-sell happens at the seat-claim, independent of payment. You must make "claim this seat" a single-winner operation.

  3. 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.

Why this piece earns its place

It’s worth being precise about what “one winner” applies to, because almost nobody buys one seat. A group buys four together, and if the single-winner operation is per seat, two buyers racing the same row can each win two of the four — no double-sell, and still two orders nobody wanted and two refunds. So the unit of the claim is the whole requested set: all or nothing, one operation that takes every seat in the order or takes none, and hands back anything it did get on a partial failure. That also closes the ordering problem hiding underneath it. If two requests grab seats one at a time in different orders, each can end up holding something the other needs and neither can finish — a deadlock you built yourself. Claiming the set in a single operation, or at minimum always in a fixed seat order, is what prevents that. Naming the race is half the answer; the other half is saying out loud what the atomic unit is.

  • 1 seat→ 1 buyer
  • 2 writers1 winner
  • nodouble-sell

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.

Booking Servicereserve seatsHold / LockRedis · TTL
New in this step: Booking Service, Hold / Lock.

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

  1. A permanent lock strands the seat forever if the buyer abandons checkout — abandoned carts would sell out the show. The hold must auto-release.

  2. 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.

  3. Charging multiple buyers for one seat then refunding all-but-one is a terrible experience and an operational mess. Reserve exclusively before payment so only one buyer can pay.

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.

Why this piece earns its place

The TTL is the one number on this page I’d expect to be pushed on, so it’s worth saying where it comes from. Set it from the measured distribution of how long real checkouts take — and from the slow tail of that, someone typing a card number on a phone, not the median — because a hold expiring under a buyer mid-payment is the worst outcome this design can generate on its own. Too short and you manufacture that case at volume. Too long and abandoned carts quietly withdraw inventory from sale: during an on-sale, seats can sit held by people who are never coming back, so the show reads as sold out while nothing has actually been sold, and those seats only return long after the crowd has gone. That’s also why I’d resist letting the client extend a hold on demand — and if extension exists at all, it’s capped in total, so the worst case stays a number you chose rather than whatever the browser asks for.

  • 5–10 minhold TTL
  • exclusivelock
  • auto-releaseon expiry

What the new pieces do

Booking Serviceservice
The heart of the system: turns 'I want seat 12C' into an exclusive hold, then a confirmed purchase.
Hold / Lockstate
Short-lived exclusive holds on seats while a buyer checks out, auto-expiring so abandoned carts free the seats.

Back of the envelope

hold = Redis lock + 5–10 min TTL
exclusive while you check out
complete ⇒ hold becomes sale
abandon ⇒ TTL auto-releases
pessimistic for hot seats
optimistic for rarely-contended ones

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.

BuyerOther BuyersAPI GatewayCatalog ServiceBooking ServiceSeat InventoryHold / Lock
The system as it stands at this step. · swipe to pan the diagram

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?

  1. A cache can lose state if a node dies, and the seat’s true state can’t drift or vanish — money is involved. The authoritative claim must be a durable, strongly-consistent write, not a cache entry.

  2. Eventual consistency here means a stale read sells an already-sold seat. Seat state is exactly where you must pay for strong consistency, even at a latency cost.

  3. 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.

Why this piece earns its place

The price of putting strong consistency here is that everything now serializes on the hottest rows in the system. Every buyer chasing the same front-row block contends on the same seats in the same partition, and under an on-sale that surfaces as lock waits — which is survivable until the waiting transactions are also holding connections, the pool drains, and bookings for entirely unrelated events start timing out behind them. So the rule is that this transaction stays tiny and local: touch the seat rows, re-check the holder, flip the state, commit. No external call ever runs inside it — the card charge in particular sits outside, which is exactly why the next step needs a saga. And the lock timeout is short with a fast failure, because a buyer told “gone, pick another” in a moment is a much better outcome than a buyer and a database both stuck waiting. Holds help here — most losers are filtered out before they ever reach the transaction — but the transaction has to survive the ones that get through.

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.

Payment Servicecharge cardBooking EventsKafka
New in this step: Payment Service, Booking Events.

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?

  1. You can’t put an external provider’s charge and your database in one ACID transaction — the provider is outside your DB. You need a sequence of steps with compensating actions, not one atomic transaction.

  2. 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.

  3. Charging before securing the seat means taking money for seats that may already be gone — mass refunds. Hold the seat first, then charge within the window, then confirm or release.

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.

Why this piece earns its place

The dangerous case isn’t the clean failure, it’s the one where you don’t know. The provider takes the charge and the response is lost on the way back. Apply the obvious rule — on timeout, release — and the seats go back into the pool, someone else buys them, and you are holding money for a seat you have now sold to a different person. So a release is only safe once you know no charge landed. On a timeout the compensating path doesn’t simply release: it reconciles with the provider against the idempotency key first, and if a charge is found it either completes the confirmation or refunds explicitly. That sets an ordering constraint worth stating too — the hold has to outlive the payment timeout, or the ordinary slow-but-successful charge races expiry on every purchase.

  • idempotentcharges
  • confirmor release
  • asyncreceipts

What the new pieces do

Payment Serviceservice
Charges the card during the hold window, then confirms the seats — or releases them if payment fails.
Booking Eventsbus
Carries confirmations, releases and receipts so payment, inventory and notifications stay coordinated.

Back of the envelope

hold → charge → confirm / release
a step + a compensating action each
idempotency key on the charge
a retry never double-bills
can’t ACID an external charge
so sequence + compensate = 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.

API GatewayBooking ServicePayment ServiceWaiting Room
New in this step: Waiting Room. · swipe to pan the diagram

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?

  1. When demand vastly exceeds the inventory itself (a million buyers, a few thousand seats), more servers just let more people fail faster — and you can’t scale a strongly-consistent seat store arbitrarily. The answer is admission control, not capacity.

  2. A free-for-all turns the on-sale into a lottery of timeouts and errors, hammering the booking core and feeling unfair. You want to shape the load into an orderly, survivable flow.

  3. 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.

Why this piece earns its place

The thing to get right is the admit rate, and I’d drive it from inventory rather than from a timer. Open loop — admit a fixed number every so often — fails in both directions. Admit faster than seats are being resolved and the wave lands all at once on the same good seats, so most of the people you just let in lose the race anyway and you’ve relocated the stampede rather than shaped it. Admit slower than the show is selling and you keep an enormous queue waiting for an event that is already gone, which is worse than telling them the truth: the queue’s job is fairness, and a fair queue for nothing is just a slower way to disappoint everyone. So the control signal is the booking core’s own state — seats still unclaimed, holds converting or expiring — and the line drains at whatever rate that supports, shows remaining inventory honestly, and closes when there is nothing left to admit anyone for.

  • 1M+in 60s
  • fairqueue
  • wavesof admits

What the new pieces do

Waiting Roomservice
A fair queue that admits buyers in controlled waves during a hot on-sale, instead of letting everyone storm at once.

Back of the envelope

1M+ buyers in 60s
vs a few thousand seats
queue + admit in waves
sized to booking capacity
admission control, not more servers
thundering herd → steady trickle

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.

BuyerOther BuyersAPI GatewayCatalog ServiceBooking ServicePayment ServiceSeat InventoryHold / LockWaiting RoomBooking Events
The system as it stands at this step. · swipe to pan the diagram

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.

Why this piece earns its place

Sharding by event works because of an assumption worth saying out loud: a purchase never spans two shows, so every hold and every claim lands inside one partition and the transaction from earlier stays single-shard. That part holds. What doesn’t hold is the idea that it balances anything. Event demand is about as skewed as distributions get — one stadium on-sale can outweigh everything else on the platform at once — so shard-per-event is a routing key, not load balancing, and a hot event sharing a partition with a thousand small ones takes those thousand down with it. So the big show gets isolated capacity of its own, and it gets moved there before the on-sale, never during. The sweeper needs the opposite instinct, because it is the only component that deletes state it didn’t create: it should act on a definite signal and nothing softer. It can’t cause a double-sell — the claim re-checks the holder — but an eager sweeper can absolutely take a seat away from someone who was still buying it.

You did it

You just designed Ticketmaster.

BuyerOther BuyersAPI GatewayCatalog ServiceBooking ServicePayment ServiceSeat InventoryHold / LockWaiting RoomBooking Events
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • A read-heavy catalog serves events and seat maps over seat inventory.
  • The core challenge is a race condition: one seat, one winner.
  • Short-lived holds (Redis + TTL) reserve seats during checkout.
  • An ACID transaction makes the final seat claim exactly-once.
  • A payment saga charges idempotently, then confirms or releases.
  • A virtual waiting room shapes the on-sale stampede into fair waves.
  • Shard by event, fight bots, and sweep orphaned holds for the real world.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Optimistic vs pessimistic locking — when each for seats?

    Pessimistic (lock the seat up front — a Redis hold or SELECT … FOR UPDATE) suits highly-contended hot seats where many buyers collide: only the lock-holder proceeds, avoiding wasted work. Optimistic (let the write proceed, reject on a version mismatch) suits rarely-contended seats where collisions are unlikely, avoiding lock overhead. Ticketmaster leans pessimistic for the few hot seats everyone wants, optimistic for the long tail.

  2. Why a saga instead of a distributed transaction across payment + inventory?

    You can’t enlist an external card processor and your database in one ACID transaction — they’re separate systems and two-phase commit across them is impractical and fragile. A saga sequences the steps (hold, charge, confirm) and defines a compensating action for each (release the hold, refund) so any failure unwinds cleanly. It trades atomicity for locally-atomic steps plus rollback — the standard pattern for cross-service consistency.

  3. How do orphaned holds get reclaimed if a booking node dies mid-checkout?

    Holds carry a TTL, so the simplest reclaim is automatic expiry — a dead node’s holds age out and the seats return. A background sweeper also scans for holds whose owning session is gone (orphaned before TTL) and releases them, and inventory is the source of truth so a hold that never converted to a sale is safe to drop. The TTL guarantees a crash can never permanently strand a seat.

  4. How do you keep scalper bots from winning all the seats?

    Layered defenses: a fair virtual waiting room (random or first-come queue position, so botting the click doesn’t help), CAPTCHAs and device/behavior signals at the gate, per-account and per-payment-method purchase limits, account verification, and rate limiting. None is perfect alone; together they raise the cost of botting. Fairness is partly queue design and partly abuse detection — you need both.

  5. Where exactly do you spend strong consistency, and why not everywhere?

    Only on seat state — the claim that flips held→sold must be strongly consistent and exactly-once, because a stale read double-sells a seat. Everything else (browsing, seat maps, receipts, notifications) can be eventually consistent and cached for scale. Strong consistency costs latency and limits scaling, so you spend it precisely where correctness is non-negotiable and nowhere else — that targeting is the senior design move.

Check yourself — the answers, and why

Eight steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. The core challenge of a ticketing system is…

    Browsing is the easy read-heavy 90%; preventing two writers from both winning a seat is the hard part.

  2. During checkout, a seat is reserved with…

    A TTL hold reserves the seat while you check out and frees it if you abandon — no permanent strand.

  3. The FINAL seat claim is made exactly-once by…

    The hold gives speed; a strongly-consistent transactional write gives the ironclad guarantee.

  4. Checkout spans an external charge, so it runs as…

    You can’t ACID an external charge with your DB; sequence steps and define rollbacks, with idempotent charges.

  5. A million-person on-sale is handled by…

    When demand ≫ capacity the answer is shaping/shedding load, not more servers.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Browse: find an event and venue, render the seat map, and see which seats are available.
  • Reserve: place a short-lived exclusive hold on chosen seats while the buyer checks out.
  • Purchase: claim held seats exactly once and flip them to sold — never double-book.
  • Pay: charge the card via an external provider, then confirm the seats — or release them on failure.
  • Survive an on-sale: admit a stampede of buyers fairly instead of letting everyone storm the door.

The qualities that shape everything

Each one names the mechanism that buys it.

Never double-sell a seat
The final claim is an ACID transaction against the inventory store: re-check the seat is still held by this buyer and flip held→sold atomically — strongly consistent, exactly-once.
Reserve while a buyer checks out
A short-lived exclusive hold (Redis lock with a 5–10 min TTL) keeps others off the seat, and auto-expires so an abandoned cart frees it.
Money-safe checkout across an external charge
Run it as a saga — hold → charge → confirm on success, release on failure — with an idempotency key so a retried charge never double-bills.
Reads scale cheaply
Browsing is a read-heavy Catalog Service over Seat Inventory; cache and replicate it aggressively so design effort goes to the contended write path.
Survive the on-sale stampede
A virtual waiting room queues buyers and admits them to booking in capacity-sized waves — admission control, not more servers.
Hot events and orphaned holds don’t break things
Shard inventory by event so each show’s hot seats live on their own partition, and run a sweeper that reclaims expired or orphaned holds.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

A strongly-consistent ACID claim over trusting the Redis hold as the record of sale

A cache can lose state when a node dies, and a stale read double-sells the seat. The hold gives speed; the transactional flip to sold is the ironclad exactly-once guarantee — you spend strong consistency exactly here.

A payment saga over one distributed ACID transaction

You can’t enlist an external card processor and your database in a single ACID transaction. Sequence the steps with a compensating action each (release the hold, refund) and make charges idempotent — atomicity you can’t get, safely approximated.

A short-lived TTL hold over a permanent lock on click

A permanent lock strands the seat forever when a buyer abandons checkout — abandoned carts would sell out the show. A TTL auto-releases, so the seat returns to the pool on its own.

A virtual waiting room over auto-scaling the booking core

When a million buyers chase a few thousand seats, more servers just let more people fail faster — and you can’t scale a strongly-consistent seat store arbitrarily. Admission control shapes the thundering herd into fair, survivable waves.

The answer, out loud

What a strong answer to “Design Ticketmaster (Event Booking)” sounds like, first question to last trade-off. It is about 6 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–4 min

    What’s easy and what’s the actual problem

    Let me start by splitting this into the part that’s easy and the part that’s actually the interview. People browse events, venues and seat maps, then they pick seats, hold them, pay, and get a confirmation. Browsing is a big read problem and I’m not going to spend our time on it. The whole question is that a seat has to go to exactly one buyer, while a crowd of people chase the same few seats in the same second. So I’ll build toward that one guarantee and let everything else hang off it.

  2. 4–9 min

    The skeleton, and the easy ninety percent

    Skeleton first. A catalog service serves events, venues and seat maps, and it reads seat state out of an inventory store that knows whether each seat is available, held or sold. That read path I’d cache and replicate hard — it’s the easy ninety percent, and being honest that it’s easy is part of the answer, because it tells you where the remaining effort should go. What I do want on record is that the availability a buyer sees is a hint. It’s the best we knew a moment ago, and the buying path is where it gets checked for real.

    Built in step 1: Browse events and seats
  3. 9–14 min

    Name the race

    Now the hard part. Two people both see 12C open, both click buy. Both read available, both write sold, and it’s been sold twice. That’s a plain race condition and I’d rather name it than dance around it, because everything after this is a way of enforcing a single winner on “claim this seat.” I’m going to enforce it in two different places for two different reasons: a short-lived hold while someone checks out, and a real transaction at the moment of purchase. They’re not redundant — they solve different problems.

    Built in step 2: The race condition
  4. 14–21 min

    Reserve while the buyer checks out

    Checkout takes minutes, so the first one is the hold. When you choose seats, the booking service takes an exclusive hold on them in Redis with a time to live, on the order of five to ten minutes. Nobody else can touch them while you’re entering a card. If you finish, the hold becomes a sale. If you close the tab, it expires on its own and the seats come back. That last part is the entire reason it’s a TTL and not a lock — abandoned carts can’t be allowed to sell out the show.

    Built in step 3: Reserve before you pay
  5. 21–27 min

    Where I spend strong consistency

    The hold is fast, but it lives in a cache and a cache can lose things. Money is involved, so the real claim is a transaction against the inventory store: inside one atomic step, check the seat is still held by this buyer and flip it to sold. That’s where I spend strong consistency, and only there. Receipts, notifications, the seat map everyone is staring at — those can all be eventually consistent. If the hold layer disappeared completely we’d have a much worse experience and still no double-sell, which tells you which of the two is the guarantee.

    Built in step 4: A seat is strongly consistent
  6. 27–34 min

    Payment is outside my database

    Then payment, which is awkward because the card processor isn’t my database. I can’t wrap a charge and a seat update in one transaction, so checkout runs as a saga: hold, charge, and on success confirm the sale and put a receipt on the event bus; on failure or timeout, release the hold so the seats go back. Every charge carries an idempotency key, so a retry is the same charge and nobody gets billed twice. Each step has something that undoes it — that’s the atomicity I can’t have, approximated safely.

    Built in step 5: Charge, then confirm — or undo
  7. 34–39 min

    The on-sale

    The on-sale is a different kind of problem. A million people in a minute against a few thousand seats — more servers don’t help, because what they’re contending for is the inventory, not my CPU, and I can’t scale a strongly consistent seat store arbitrarily anyway. So the gateway diverts buyers into a virtual waiting room and admits them into booking in waves. That’s admission control rather than capacity. It also makes fairness something I designed on purpose, instead of a reward for whoever had the fastest connection.

    Built in step 6: Tame the on-sale rush
  8. 39–43 min

    Hot shows, bots, and crashed checkouts

    Two more things before I close. Inventory gets sharded by event, so one show’s hot seats sit on their own partition instead of dragging every other event down with them. And scalpers — that’s layered: fair queue positions, CAPTCHAs and device signals at the gate, limits per account and per payment method. None of it works alone; together they make botting expensive enough to be worth less. I’d also run a sweeper that reclaims holds left behind by a crashed checkout, so a dead node never strands a seat permanently.

    Built in step 7: Scale, fairness & failure
  9. 43–45 min

    The trade-off, and what’s left

    So the trade-off I’m making is that I pay latency and scaling headroom in exactly one place, the seat claim, and buy cheapness everywhere else with caching and asynchrony. With more time I’d go deeper on two things. One is what happens when a hold expires while a charge is still in flight, because that’s where I think the real bugs live. The other is what fairness actually means in that queue — random draw versus first come — since that’s a product decision as much as an engineering one.

What this teaches

Learn system design by building an event-ticketing system like Ticketmaster step by step. An interactive guide covering the read-heavy catalog, the seat-booking race condition, short-lived holds with TTL, strongly-consistent ACID seat claims, the payment saga, a virtual waiting room for on-sale stampedes, sharding and bot defense.

Key takeaways

  • A read-heavy catalog serves events and seat maps over seat inventory.
  • The core challenge is a race condition: one seat, one winner.
  • Short-lived holds (Redis + TTL) reserve seats during checkout.
  • An ACID transaction makes the final seat claim exactly-once.
  • A payment saga charges idempotently, then confirms or releases.
  • A virtual waiting room shapes the on-sale stampede into fair waves.
  • Shard by event, fight bots, and sweep orphaned holds for the real world.

Concepts covered

  • How does Ticketmaster avoid double-booking?
  • Browse events and seats
  • The race condition
  • Reserve before you pay
  • A seat is strongly consistent
  • Charge, then confirm — or undo
  • Tame the on-sale rush
  • Scale, fairness & failure
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 / 65 System Designs done
NextDesign a Key-Value Store Closest system design to this one

Explore the topic

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

More System Designs