Library
32 min read

Design Ticketmaster

A stadium tour goes on sale at 10am. Two million fans want sixty thousand seats, and each fan does not want "a seat", they want G5 in the floor section, next to their friend in G4. Every seat is its own tiny auction, and it closes in milliseconds.

Watch one fan get from the line to a ticket.

FansWaiting roomAPISeat map cacheHold serviceSeat storePayment gatewayOrders and tickets

Line up. At 10am two million fans land on the waiting room. It gives each one a place in line and does nothing else.

The picture is two systems with very different promises. The top half, the seat map, may be a little out of date, and it takes nearly all the reads. The bottom half, the hold, must be exact for every single seat, and it takes very few writes. Most of this page is about keeping those two promises separate.

If you have read IRCTC tatkal, here is the difference that matters. There, seats in a class are interchangeable, so the whole fight is over one number per train. Here the fan chooses seat G5, so every seat is its own row and its own race, and a group of seats has to be won together. The same problem shows up when you book two seats next to each other for a first day, first show at a cinema, or tickets for an IPL final.

The real Ticketmaster

This page builds the core of a ticketing site like Ticketmaster: choose seats from a map, hold them while you pay, and survive an on-sale where demand is many times the supply. The numbers are interview assumptions, not Ticketmaster's. Two real things worth knowing: Ticketmaster puts popular on-sales behind a queue and runs Verified Fan registration to filter out bots before the sale, and it said the Eras Tour presale in November 2022 drew 3.5 billion system requests, four times its previous peak, which it blamed on bots and fans without invite codes. Bot defence is a large real system that this page names but does not build.

A few words used all through this page:

  • A hold takes seats off sale for a few minutes while one fan pays. If they never pay, the seats go back.
  • The seat map is the picture of a section with every seat coloured free, held or sold.
  • The waiting room gives each fan a place in line before they may shop, like the token machine at a bank.
  • An on-sale is the moment a popular event's tickets go on sale, announced in advance.

Step 1: Understand the problem#

Ask about the seats before you ask about the traffic. Whether a fan picks a specific seat or just "two tickets in the upper tier" decides what the contended thing is, and everything else follows from that.

You askThey sayWhat it settles
Do fans pick specific seats, or just a section and a count?Specific seats from a map, with a "best available" button as well.Every seat is its own row and its own race. The contention is spread over sixty thousand rows instead of one counter, but a group of seats now has to be claimed all at once.
How big is the on-sale?About two million fans for about sixty thousand seats, at a time announced weeks ahead.Thirty or more fans per seat, so most fans leave without a ticket. Like tatkal, the front door is a rejection system and the booking path behind it is small.
How long can a fan sit on seats while paying?A few minutes, with a countdown. Assume 8.A hold with an expiry on every seat row, and a rule for when time runs out in the middle of a payment.
Can the seat map be out of date?By a couple of seconds, yes. The hold must never be wrong.Two promises. The map is a cache that can lie a little, and the hold is checked against the seat rows every time. A fan tapping a seat that just went is answered with a 409 and a fresh map.
How many tickets can one order have?Up to 8, and the seats are usually next to each other.All or nothing claims across several rows. A fan who wanted a pair and got one seat has been handed something useless that is now off sale.
What about bots and resellers?A real problem. Assume registration filters most of them before the sale.A per account limit on tickets and on active holds, enforced at the hold. Full bot detection is out of scope, but the design must not reward a script that holds seats it never pays for.

What you are building, and what you cut#

Functional requirements
What the system does.
  • Browse an event and see the seat map by section. Almost all of the reads. Allowed to be a couple of seconds stale.
  • Hold chosen seats, or the best available block, for 8 minutes. The hard part. Exact, per seat, and all or nothing for a group.
  • Pay and turn the hold into tickets. Money moves in another company, which is the payment rail problem again.
  • Survive the on-sale with a waiting room. Thirty fans per seat arrive in the same minute.
Non-functional requirements
How well it has to do it. The numbers you commit to.
  • No seat is ever sold twice.
  • About two million fans in line for a 60,000 seat show, selling out in about ten minutes.
  • A hold or a "that seat just went" answer within about a second.
  • Fair order of admission: joining the line earlier should count.
Out of scope
What you cut, and say so out loud.
  • Bot detection and fan registration. Named as real, and upstream of this design.
  • Resale and ticket transfer. A second marketplace with its own rules.
  • Pricing that changes with demand. It changes the number on the seat, not how the seat is claimed.
  • Venue setup and seat map drawing. Seat maps are created once per venue, long before the sale.

Back of the envelope#

A small version first. A 60,000 seat stadium, four tickets per order, is 15,000 orders. If it sells out in ten minutes, that is 1,500 checkouts a minute, which is 25 a second. That number is small. The people trying to become one of those 25 a second are not. (The back of the envelope chapter covers the method.)

Fans against checkouts
2000 thousand
60000
4
10
Orders in the whole venue60,000 / 4 = 15,000
Fans per order2,000,000 / 15,000 = 133 to 1
Checkouts per second to sell out15,000 / (10 x 60) = 25.0
Fans who leave without tickets1,985,000
25 checkouts a second
is all the booking path has to do

The booking path is tiny: 25 new checkouts a second would sell the venue out in 10 minutes. Everything big on this page exists to keep 2,000,000 people from touching it at once, while the few who are let in see a map that is close enough and a hold that is exact.

Read the first and third rows together. The writes that matter are a few dozen a second. The waiting room and the seat map cache carry everything else, and the hold service never sees the crowd at all.

Step 2: Propose the high level design#

The core entities
  • EventOne show at one venue on one date, with an on-sale time and a per account ticket limit.
  • SeatOne row per seat per event, like FLR-G5. The thing that is contended.availableheldsold
  • HoldWhich seats one fan is sitting on, and until when.activepayingconfirmedexpired
  • OrderA paid hold. Issued as tickets, and never undone by an expiry.
Never breaks: A seat is sold to one fan, and a seat that was paid for is never released.

The API#

Four calls. The hold is the one that matters, and its 409 is designed as carefully as its 201.

GET/v1/events/{id}/sections/{section}/map
returns 200 { version: 91442, seats: [{ id: "FLR-G5", state: "available" }, ...] } with Cache-Control: max-age=2
Why: Per section, not the whole venue, because a fan looks at one section at a time and a section is small enough to rebuild every couple of seconds. The version lets the client ignore an older map that arrives after a newer one.
POST/v1/events/{id}/holds
{
  "seats": ["FLR-G4", "FLR-G5"],
  "admissionToken": "eyJ...",
  "idempotencyKey": "h_5e1a..."
}
returns 201 { holdId, expiresAt } or 409 { taken: ["FLR-G5"], map: {...} }
Why: The 409 names the seat that went and carries a fresh map for the section, so the fan picks again without a second request. A 409 that says only "conflict" sends the fan back to a stale map to tap another taken seat.
POST/v1/events/{id}/holds:best
{ "section": "FLR", "count": 4, "together": true, ... }
returns 201 { holdId, seats: ["FLR-K7", ...], expiresAt } or 409
Why: The server picks the seats, so it can choose ones nobody else is reaching for. During an on-sale this path collides far less than two million people tapping the same front row.
POST/v1/holds/{holdId}/checkout
{ "paymentToken": "...", "idempotencyKey": "c_77b0..." }
returns 200 { orderId } or 202 if payment is still unresolved, or 410 if the hold expired
Why: A 410 says the hold is gone for good and no money was taken. A 202 is the honest answer while the gateway has not replied, and the seats stay held until it does.

The admission token comes from the waiting room and is checked on every hold, so a fan cannot skip the line by calling the API directly. The idempotency keys mean a double tap on a bad connection returns the first hold instead of a second one (idempotency keys shows how).

The data model#

Two tables in the seat store, both keyed by event first, so every row for one show sits on one database machine and a hold for several seats is one local transaction.

seatsrelational, sharded by event_id
event_idbigintPKShard key. One show, one machine, so a group claim never spans two databases.
seat_idvarchar(16)PKSection, row and number, like FLR-G5. Created once from the venue map.
sectionvarchar(8)IDXThe seat map is read one section at a time.
price_tiersmallint
stateenumavailable, held or sold.
hold_iduuidWho holds it. Null when available.
held_untiltimestampThe claim treats a held seat whose time has passed as free. No job has to run first.
Sample row
8812 | FLR-G5 | FLR | 1 | held | h_7a2c | 10:08:00
8812 | FLR-G6 | FLR | 1 | available | null | null
Sixty thousand rows per show, each one its own race. The claim updates several of them in one transaction, and the row count it gets back decides the answer.
holdssame shard as the seats, keyed by event_id first
event_idbigintPK
hold_iduuidPK
user_idbigintIDXFor the per account limit: one active hold per fan per event.
idempotency_keyvarchar(64)UQA second tap returns this hold instead of claiming more seats.
seat_idsarrayThe group. Claimed together, released together.
stateenumactive, paying, confirmed or expired.
expires_attimestampSame value as held_until on its seats, written in the same transaction.
Sample row
8812 | h_7a2c | 4471 | h_5e1a... | FLR-G4, FLR-G5 | active | 10:08:00
The hold row is what the countdown on the fan's screen shows, and what checkout looks up. The seat rows are what the next claim checks.

The whole system on one whiteboard#

Figure 1 is the picture at the top, laid flat, with the CDN and the job that rebuilds the seat map added.

Figure 1. The crowd stops at the waiting room and the map cache. Behind them, the hold service does a few dozen small transactions a second on one shard per show, and that is the only place a seat changes hands.

Walking Figure 1:

  1. Fans join the line. The waiting room hands each one a signed token with a position and does no other work, so it can take the whole crowd. The event page itself comes from the CDN and never reaches us.
  2. The room admits fans at the rate checkouts finish, about 1,500 a minute in the napkin above, and only an admitted fan's token is accepted by the API.
  3. The fan opens a section. Its map comes from the cache, rebuilt every couple of seconds from a replica of the seat store by the map builder. It may be a little behind, on purpose.
  4. The fan taps two seats and asks to hold them.
  5. The hold service claims both seat rows in one transaction, each only if it is free or its old hold has run out. Both rows change, or the transaction is rolled back and the fan gets a 409 with a fresh map.
  6. Checkout charges the card through the payment gateway, with the seats already held. Starting payment moves the hold to paying and extends it once.
  7. Payment clears, the seats become sold and the order and tickets are issued.

There is no box for expiring holds. That is deliberate, and the first deep dive shows why.

Your answer

Two fans tap seat G5 in the same millisecond. One also wants G4, the other also wants G6. What has to be true of the claim for both of them to get a sensible answer?

Step 3: Design deep dive#

Holding G5, and only G5#

The contended thing is one seat row. Three fans can want G5 at once, and the claim has to pick exactly one of them without a slow lock and without a background job.

How does the hold service claim seats so that exactly one fan gets each one?

  1. First try
    A lock per seat in Redis, with a TTL
    SET seat:8812:FLR-G5 to the hold id, only if it does not exist, with an 8 minute expiry. The key vanishing is the hold expiring.
    Where it breaks: Now the truth about a seat lives in two places, Redis and the seat store, and they will disagree after a failover or a crash between the two writes. A Redis failover can also lose a lock that was just acknowledged, and two fans then hold the same seat. Fast, and wrong in exactly the case that matters.
  2. Better
    Lock the seat rows, then check, then write
    SELECT the seats FOR UPDATE, check they are free, mark them held, commit. Correct.
    Where it breaks: Fine at 25 checkouts a second. But a held seat stays held until something marks it free, so you need a sweeper job, and when the sweeper lags, seats whose time is up sit off sale while fans are told the section is full.
  3. What you ship
    One conditional update, with the expiry inside the condition
    UPDATE the seats SET held, hold id and held_until, WHERE they are in the group AND (state is available OR their held_until has passed). Count the rows changed. All of them means the hold is yours. Fewer means roll back and return 409. An expired hold is free the instant its time is up, because the claim itself checks the time.

Try it. In "seat by seat", hold for Rahul and then for Priya and look at G6. In "all or nothing", do the same, then move the clock past 10:08 without paying, and hold for Priya again.

Two fans, one row, overlapping seats
10:00
G1
free
G2
free
G3
free
G4
free
G5
free
G6
free
G7
free
G8
free
Rahul wants G4 and G5
Priya wants G5 and G6
Hold for Rahul, then for Priya. Then move the clock past 10:08 and try Priya again, without paying for Rahul.
Holds last 8 minutes here. A real site shows the countdown, and the claim is one conditional UPDATE over the seat rows inside one transaction.

Two things the widget shows. First, a group of seats is one claim: half a pair is worse than nothing, because the fan cannot use it and nobody else can buy it for 8 minutes. Second, expiry needs no job. A sweeper can still run to tidy rows and update the seat map, but a late sweeper only delays the tidying, never a sale. That is the difference from a sweeper that is part of correctness, like the tatkal one.

The race itself is the classic lost update, settled the same way as tatkal's single row: check and change in one statement, and let the row count decide. What is new here is that one statement now touches several rows.

Two groups, opposite order

Fan A claims G5 then G6 while fan B claims G6 then G5. Each transaction holds one row and waits for the other: a deadlock. The database will detect it and kill one transaction, which is safe but slow and noisy at peak. Lock the rows of a group in one fixed order, sorted by seat id, and two overlapping claims queue behind each other instead.

What happens when time runs out in the middle of a payment? The fan has typed their card number at 10:07 and the gateway answers at 10:09. Starting checkout moves the hold to paying and pushes held_until out once, by enough to cover a slow payment. And a hold whose payment outcome is unknown is never released: the system asks the gateway about it by its reference, then sells the seats or frees them. That is the rule from UPI payments, where a timeout means "unknown", not "failed".

The seat map is allowed to lie#

Two million people refreshing a seat map would bury the seat store if every refresh read it. So the map has a different promise from the hold, and saying that out loud is half of the answer.

Where does the seat map come from?

  1. First try
    Read the seat rows on every request
    SELECT the state of every seat in the section, for every fan, every time.
    Where it breaks: Thousands of admitted fans refreshing every few seconds is thousands of section scans a second on the one machine that also has to run the claims. The reads slow down the writes that matter.
  2. Better
    Cache the whole venue, refresh it every minute
    One cached map per event, rebuilt in the background.
    Where it breaks: A minute is far too stale during an on-sale. Almost every seat a fan taps has already gone, so most holds become 409s and every 409 sends the fan back for another stale map.
  3. What you ship
    Cache per section, rebuilt every couple of seconds from a replica
    A section of a few thousand seats is small and cheap to rebuild. Each map carries a version so the client keeps the newest one, and every 409 returns the section's current map from the primary, so a fan who lost a race sees the truth straight away.

So a fan can tap a seat that shows as free and get "that seat just went". That is not a bug. The map promised "roughly", the hold promised "exactly", and the 409 is where the two meet. Deciding consistency per feature like this is the idea in strong vs eventual consistency.

The follow up you will get

"Why not push seat changes to every open map over a WebSocket?" You can, and for a small venue it is pleasant. At an on-sale, every hold would fan out to every fan looking at that section, which is a broadcast problem bigger than the hold traffic that caused it. A map that refreshes every couple of seconds, plus a fresh section in every 409, gives almost the same experience for a fraction of the cost.

Held is not sold#

The waiting room works like the tatkal queue: signed positions, and admission at the rate the booking path can serve. Two things are different here, and both come from holds.

First, admission follows checkouts, not seats. An admitted fan spends minutes choosing and paying, so the room admits roughly as many fans as are leaving checkout each minute, about 1,500 at the napkin's numbers. Admit faster and fans pile into a store with nothing left to hold. Admit slower and seats sit unsold.

Second, the moment every seat is held is not the moment the show is sold out. A held seat comes back if its fan does not pay, and some always do not. So the line must not tell fan number 200,000 "sold out" while thousands of seats are only held. It says "everything is held right now, seats may come back", keeps admitting slowly as holds lapse, and switches to sold out only when held seats reach zero.

Every seat is held and nothing is sold yet. What does the line do?

  1. First try
    Close the line: sold out
    No seats are available, so tell everyone the show is sold out.
    Where it breaks: Thousands of held seats come back over the next minutes, with nobody left in line to buy them. They go to whoever happens to refresh, or to bots that never left.
  2. Better
    Keep admitting at full speed
    Let people in so they can grab seats as holds expire.
    Where it breaks: Thousands of fans in the store, watching a map with no free seats, tapping every seat that flickers free. The hold service turns into a 409 machine and the line stops meaning anything.
  3. What you ship
    Admit as fast as seats come back, and say why
    Track how many seats return from expired holds each minute and admit about that many fans. Tell the line honestly that seats are held and may return. Switch to sold out when nothing is held.

Break it#

On-sale
09:59
09:5910:00front rowgateway slowrepaired
Healthy. Two million fans hold a position and stare at a countdown. The waiting room serves a static page and a token. Nothing behind it has started.
Read every step as text
  1. 09:59 · Healthy. Two million fans hold a position and stare at a countdown. The waiting room serves a static page and a token. Nothing behind it has started.
  2. 10:00 · Degraded. Admission opens. Every admitted fan loads a section map and refreshes it, so map reads jump, but they are cache reads of a few sections. The seat store sees only claims, a few dozen a second.
  3. front row · Down. Everyone taps the same front rows. Hundreds of claims a second land on a few hundred seat rows, and most lose, which is correct. But each 409 sends a fan back for another map, so a lost race costs two requests, and every loser taps the next seat everyone else can see too.
  4. gateway slow · Degraded. The payment gateway slows to 20 seconds. Holds in paying are extended, so nobody loses seats mid payment, but seats stay off sale longer and fewer come back each minute. If the room keeps admitting at the old rate, fans arrive to a store with nothing to hold.
  5. repaired · Healthy. Each 409 carries the fresh section map, and best available is the default button, because a server choosing seats spreads claims away from the front row. The room admits at the rate checkouts actually finish, so a slow gateway slows admission instead of flooding the store.

The front row case is worth saying unprompted. It looks like a database problem and it is not: the seat store handles a few hundred tiny transactions a second easily. The waste is the retry loop on the client, and the fix is a better 409 and letting the server pick seats.

Trade-offs#

ChoiceWhat you gainWhat you payPick it when
Claims in the seat store, not in Redis locksOne place holds the truth about a seat, inside a real transaction.Every claim is a database write, a few milliseconds instead of under one.Always for tickets. At a few dozen checkouts a second, the speed of Redis buys nothing and its failover can sell a seat twice.
Expiry checked inside the claimA seat is free the instant its hold runs out, with no job on the critical path.The seat map shows expired holds as held until the next tidy up or rebuild.Any hold with a timer. Keep a sweeper for tidying, never for correctness.
A seat map that may be two seconds staleThe crowd reads a cache, and the seat store only runs claims.Fans sometimes tap a seat that just went and get a 409.Whenever reads outnumber claims by orders of magnitude, which at an on-sale they do.
Best available as the defaultThe server spreads claims across free seats, so far fewer fans collide.Fans who care which seat they get have to go to the map.During the on-sale rush. Afterwards, let the map lead.

The level bar#

One moment from this round, answered three ways. Pick the one you think is staff level before the labels appear. (What changes at senior and staff explains the gap.)

Pick the staff level answer

InterviewerA fan wants four seats together, and no block of four is free in their section. What does the system do?

Show which is which without guessing
  • Answer A: Senior. Moves the search to the server, which is right. Stops when the answer is "none".
  • Answer B: Mid level. True, and it loses a sale that the fan would probably have taken in another shape.
  • Answer C: Staff. Turns a dead end into an offer that cannot be snatched while the fan decides, and sees the same feature as the fix for the on-sale contention.

Then run the question cold, with a 45 minute timer and no notes, and score the run on the five things this question is really testing.

Score your last run
0 / 10
  1. The contended thing
  2. The claim
  3. Expiry
  4. The seat map
  5. The on-sale
Pick 0, 1 or 2 on every row. Be the strict interviewer, not the kind friend.

Interview replay#

Interviewer
Two million fans, sixty thousand seats, the sale opens at 10. Where do you start?
Checking whether you start with the crowd or with the seat.
You
With the seat, because fans pick specific seats. That makes every seat its own row and its own race, and a group has to be claimed together. Then the numbers: sixty thousand seats at four per order is fifteen thousand checkouts, so selling out in ten minutes is about 25 a second. The booking path is small. The design is mostly about keeping two million people away from it.
Finds the contended thing first, then uses one number to size the whole design.
Interviewer
Two fans tap G5 at the same moment.
The core question.
You
One conditional update over the seats in each group: set them held with an expiry, where each is available or its hold has run out. The row count decides. If I asked for two seats and changed two rows, the hold is mine. If I changed one, I roll back and return a 409 that names the seat and carries a fresh section map. I lock the rows in seat id order so two overlapping groups do not deadlock.
Covers the race, the group and the deadlock in one answer.
Interviewer
Why not Redis locks with a TTL? Much faster.
Testing whether you value speed you do not need.
You
Speed is not the constraint, 25 checkouts a second is nothing for a database. A Redis lock puts the truth about a seat in a second place that can disagree with the seat store, and a failover can lose a lock that was just granted, which sells a seat twice. I would rather pay a few milliseconds for one source of truth.
Rejects the faster option with the number that shows speed is not needed.
Interviewer
Every seat is held and nothing is sold yet. What does fan number 200,000 see?
The question most candidates have not thought about.
You
Not "sold out". Held seats come back when fans do not pay, so the line says everything is held right now and seats may return. The room keeps admitting about as many fans as seats come back each minute, and only shows sold out when nothing is held.
Separates held from sold, and ties admission to the real return rate.
Interviewer
How long should a hold last?
Open ended, checking for invented precision.
You
I would not pick it from first principles. Too short and fans lose seats while typing a card number. Too long and seats sit off sale while nobody pays. I would look at how long checkouts really take, cut somewhere in the tail, and I would expect it to be a few minutes. Starting payment should extend it once, so the timer never runs out mid payment.
Gives the trade, a method and a range, and the rule for the dangerous edge.

Checkpoint#

Checkpoint

1. A 60,000 seat venue sells four tickets per order and sells out in ten minutes. About how many checkouts a second does the booking path handle?

2. A fan's hold ran out at 10:08 and the sweeper has not run yet. At 10:09 another fan tries to hold the same seats. What happens?

3. Same system, but the show is general admission: standing tickets, no seat numbers. What changes first?

Worth memorising
  • 60,000 seats at four per order is 15,000 checkouts. Selling out in ten minutes is about 25 a second, so the booking path is small and the front door is huge.
  • Every seat is its own row and its own race. All the seats in an order are claimed in one transaction, or none are.
  • A hold is free the moment its time is up. The claim checks the expiry, so a late sweeper delays the tidying, never a sale.
  • Held is not sold. While seats sit in holds, the line says "everything is held", not "sold out".
Say this in 60 seconds

Ticketing looks like tatkal, but fans pick specific seats, so every seat is its own row and its own race, and a group of seats has to be won together. The writes are small: sixty thousand seats at four per order is fifteen thousand checkouts, about 25 a second to sell out in ten minutes. So the design is a waiting room that admits fans at the rate checkouts finish, a seat map cached per section for a couple of seconds, and a hold service that does one conditional update over the seats in an order: mark them held with an expiry where each is available or its old hold has run out, and decide by the number of rows changed, rolling back on a partial match. Expiry lives inside that condition, so a seat is free the instant its time is up, and a sweeper only tidies. Starting payment extends the hold once, and a hold with an unknown payment is never released until the gateway answers. Every 409 carries a fresh section map, and best available is the default during the rush because a server picking seats collides far less than people tapping the front row. The failure I would call out is the moment every seat is held: that is not sold out, so the line keeps admitting at the rate held seats come back.