This gives away every stage. For a clean attempt, practise it first, then come back.
The short version
- Browsing can be a few seconds stale; claiming a seat must be exactly right. Design the two paths differently.
- Claim seats with one conditional update that only works if they’re still free. Never “check, then book”.
- A hold is valid until its expiry time, checked on every read. Don’t rely on a cleanup job.
- During a big sale, a waiting room lets fans in at the speed payments can finish.
- Payments can finish after a hold expires. Use idempotency keys, and refund if the seats are gone.
The 45 minutes
Before we start
Think about booking a seat for a big cricket match or concert. You open the seat map, tap two seats, and a timer starts: “Complete payment in 10:00”. Behind that simple screen are three hard problems.
First, two people can tap the same seat at the same moment, and only one can get it. Second, the timer: if you walk away, those seats must go back on sale on time, even if something crashes. Third, the crowd: for a big show, millions of people arrive in the same minute, far more than there are seats.
Same order as always: requirements, quick maths, data and API, a design that grows, then deep dives.
Minutes 0–5Requirements
Interviewer: Design a ticket booking system like BookMyShow for concerts and cricket matches.
You: Do fans pick exact seats, or just a section?
Interviewer: Exact seats, from a seat map.
You: How big are the biggest sales?
Interviewer: A big concert: about 2 million fans trying for 50,000 seats when sales open.
You: How long can someone hold seats while paying?
Interviewer: 10 minutes. Then the seats go back on sale.
You: Is selling one seat to two people ever acceptable?
Interviewer: Never.
You: How do payments work?
Interviewer: Through a payment gateway, with cards and UPI. Sometimes it’s slow or doesn’t answer.
You: Do we need to be fair and stop bots?
Interviewer: Yes. Real fans should get a fair chance, and one person can buy at most 6 tickets.
Functional requirements
- Browse events and see which seats are free.
- Pick seats and hold them for 10 minutes.
- Pay, and get confirmed tickets.
- During big sales, a waiting room lets fans in fairly.
Nice to have: limit tickets per person. Out of scope: refunds, reselling and changing prices based on demand.
Non-functional requirements
- Never sell a seat twice. This is the one thing that must always be exactly right.
- Handle the rush: 2 million fans in the first few minutes of a sale.
- Holds expire on time, even if nothing cleans them up.
- Browsing stays up even when booking is at full capacity.
- Fair, and hard for bots.
Minutes 5–10Back-of-the-envelope estimates
You: 2 million fans arriving over about 10 minutes is 3,300 a second.
You: If they all sat on the seat map refreshing every 5 seconds, that’s 400,000 reads a second. Huge.
You: But here’s the thing: there are only 50,000 seats. At about 2.5 seats per booking, that’s 20,000 bookings in total. And a payment gateway might handle around 300 payments a second, so even perfectly, the whole sale is done in about a minute of payments.
You: So letting 2 million people fight over 50,000 seats at once doesn’t sell anything faster. It just creates load and frustration.
That’s the insight to say out loud: payments, not servers, decide how fast seats can sell. So we should let people in at about that speed, and keep everyone else calmly waiting.
Minutes 10–15Core entities and the API
Core entities
- Event: the match or concert, its venue and its time.
- Seat: one seat for one event, and its state: free, held (by whom, until when) or booked.
- Hold: a fan’s claim on some seats, with an expiry time.
- Booking: a paid, confirmed purchase of seats.
- Payment: the charge at the payment gateway, linked to the hold.
The API
/v1/events/881/seatsThe seat map. It may be a few seconds old.
- 200
{ "sections": [ { "name": "North Stand", "free": ["A12", "A13"] } ] }
/v1/events/881/holdsTry to hold seats for 10 minutes. You get all of them, or none.
{ "seats": ["A12", "A13"] }- 201
{ "hold_id": "h_55", "expires_at": "2026-11-10T10:12:00Z" } - 409One of these seats was just taken. Pick again.
/v1/holds/h_55/confirmPay and turn the hold into a booking. The key stops a retry from charging twice.
Idempotency-Key: pay_h_55
{ "payment_token": "tok_…" }- 201
{ "booking_id": "b_9001", "tickets": ["A12", "A13"] } - 410The hold expired before payment finished.
Notice that holding is “all or nothing”. If you want A12 and A13 and only A12 is free, you get neither, and can pick again. Nobody wants one of two seats they asked for.
Minutes 15–25The design, built up
Stage 1One server: check the seat, then book it
You: The simplest version: one app server and one Postgres table of seats. To book, check whether the seat is free, and if it is, mark it as booked.
This looks fine until two fans tap the same seat at the same moment. Both checks happen before either booking is written, so both see “free”, and both book. Try it:
Stage 2Hold seats with one atomic claim, then pay
You: The fix is to never split “check” and “book” into two steps. Instead, one statement: set these seats to held for me, until ten minutes from now, but only if they’re still free. The database runs competing statements one after another, so only one can succeed. If fewer rows changed than seats requested, we undo and ask the fan to pick again.
You: Then payment: the booking service charges the fan through the payment gateway, and marks the seats booked, again only if the hold is still theirs.
You: Expiry: each held seat stores its hold-until time. Anywhere we check if a seat is free, a hold whose time has passed counts as free. So holds expire exactly on time, even if nothing cleans them up. A small background job still clears old holds so the seat map looks tidy.
An operation that happens in one indivisible step like this is called atomic. Lesson 1.4 shows how databases make that possible.
Stage 3The flash sale: a waiting room and a cached seat map
You: Now 2 million fans arrive at ten o’clock. Two changes.
You: A waiting room in front of booking. Fans who arrive before the sale opens are put in random order, so being half a second faster doesn’t matter. Later arrivals join the back. The room lets people through at about the rate payments can finish, and everyone else sees their place in line and an estimated wait.
You: And the seat map comes from a cache, rebuilt every few seconds, while event pages and images come from a CDN. Browsing never touches the seats database. A map a few seconds old is fine, because the hold step checks the real state anyway.
This is not just a nice idea. Ticketmaster said its 2022 Taylor Swift presale saw about 3.5 billion system requests, four times its previous peak, much of it from bots, and the site struggled badly. A waiting room turns a stampede into a queue.
Stage 4After payment: tickets and emails in the background
You: Once a booking is confirmed, the fan shouldn’t wait for tickets to be generated and emails to be sent. The booking is saved, a “booked” event goes onto a queue, and workers create the tickets and send the confirmation email.
(This is the same queue idea as the notification system, and lesson 4.1.)
Checking the design against the requirements
| Requirement | Where the design handles it |
|---|---|
| Browse and see free seats | CDN for pages; seat map from a cache, a few seconds old |
| Hold seats for 10 minutes | One atomic claim with a hold-until time |
| Pay and get tickets | Payment gateway with an idempotency key; tickets made in the background |
| Waiting room for big sales | Admits fans at about the speed payments can finish |
| Never sell a seat twice | Seats are only claimed with “only if still free” updates |
| Handle the rush | Waiting room in front; browsing served from caches |
| Holds expire on time | Expired holds count as free on every check |
| Browsing stays up | Separate cached read path; never touches the seats database |
| Fair and hard for bots | Random order at the start, per-account limits, bot checks at the entrance |
Deep diveNever selling a seat twice
Interviewer: Walk me through the options for claiming a seat.
Bad solutionCheck, then book
Read “is A12 free?”, then write “A12 is mine”. Two requests can both read “free” before either writes. This is the classic race, and during a big sale it happens constantly.
Good solutionLock the row first
SELECT … FOR UPDATE locks the seat’s row, so a second request waits until the first one finishes. Correct. But during a big sale, many requests pile up waiting on the most popular rows, and a slow request holds everyone up.
Great solutionOne conditional update
UPDATE seats SET status = 'held', hold_id = …, hold_until = now() + 10 minutes WHERE event_id = 881 AND seat_id IN ('A12', 'A13') AND (status = 'free' OR hold_until < now()), then check how many rows changed. Two rows means you got both seats. Fewer means someone beat you, so roll back. The database settles it in a single step, there’s no waiting around, and doing it inside one transaction makes multi-seat holds all-or-nothing.
Deep diveHolds that expire on time
Interviewer: How do you make sure abandoned holds go back on sale?
Bad solutionA cleanup job is the only thing that frees holds
If the job runs every minute, seats stay stuck for up to a minute. If the job crashes, they stay stuck until someone notices. During a sale, that’s lost revenue and angry fans.
Good solutionStore an expiry time and check it everywhere
Each held seat has hold_until. Every check for “is this seat free?” treats a hold whose time has passed as free. Expiry is now exact, whatever happens to background jobs.
Great solutionExpiry time, plus tidy-up, plus a little extra time while paying
Keep the expiry check, and let a background job clear old holds every few seconds so the seat map catches up. And when a fan starts paying, extend their hold by a few minutes, so a normal payment never fails just because the timer ran out mid-payment.
Deep diveThe 10 o’clock rush
Interviewer: Two million fans arrive at 10:00. What happens, step by step?
You: Without protection, everyone hammers the seat map and the booking service at once. The database slows down, holds time out, payments fail, and fewer tickets get sold, not more.
You: With a waiting room: fans who arrived early are shuffled into random order when the sale opens; later fans join the back. The room lets in a few hundred people a second, about what payments can handle. Everyone else sees “you’re number 48,210, about 12 minutes”.
You: Behind the waiting room, the booking service only ever sees a manageable flow. The seat map is served from a cache that refreshes every few seconds. And when everything’s sold, the waiting room can tell the people still in line right away, instead of letting them in to find nothing left.
Deep divePayments that finish too late
Interviewer: The payment gateway takes 12 minutes to answer. By then the hold has expired and someone else has the seats. What now?
You: First, reduce how often it happens: extend the hold when payment starts.
You: Second, when payment does succeed, mark the seats booked with the same kind of conditional update: only if the hold is still mine. If that fails, the seats are gone, so refund the payment automatically and tell the fan.
You: Third, never charge twice. Every confirm request carries an idempotency key built from the hold. If the app times out and retries, the gateway sees the same key and doesn’t charge again. And if we don’t know whether a payment went through, we ask the gateway for its status before doing anything else.
Lesson 7.3 shows that “we don’t know if it worked” moment, which is the hardest part of payments:
Deep diveBots and scalpers
Interviewer: How do you keep bots from buying everything?
You: No single trick works, so I’d layer a few. Only logged-in, verified accounts can enter the waiting room. Bot checks happen at the entrance, not during checkout, so real fans aren’t slowed down at the worst moment. Limit tickets per account and per payment card. Rate-limit by account and device (lesson 6.3). And for the biggest events, give pre-registered fans their own presale with personal codes.
The complete architecture
Here’s everything together: the full design from the last stage, with every part we added along the way. If you only remember one picture from this question, make it this one.
How one request travels through it:
- Fans browse the event page and seat map. Pages come from the CDN, and the seat map from a cache refreshed every few seconds.
- When a big sale opens, buyers enter a waiting room. It lets people through at about the rate payments can be completed, and shows everyone else their place in line.
- When it’s your turn, you pick seats. The booking service claims them in one atomic database update that only succeeds if every seat is still free, and sets a 10-minute expiry.
- You pay. The booking service charges you through the payment gateway with an idempotency key, then marks the seats booked, but only if your hold is still valid.
- Expired holds simply count as free. A background job also releases them, so the seat map catches up.
- A booking event goes on a queue, and workers create tickets and send the confirmation email.
How this answer gets scored
Interviewers don’t tick boxes, but their notes on this question tend to land on the same few moments. Here’s what each one sounds like from the other side of the table.
| Moment | What earns credit | What costs you |
|---|---|---|
| The first five minutes | Separating “see seats” from “claim a seat” | Treating all data as needing the same consistency |
| The numbers | Noticing payments limit how fast seats can sell | Sizing servers for 2 million people at once |
| Claiming seats | One conditional update, all-or-nothing | Check, then book |
| Holds | Expiry time checked on every read | A cleanup job as the only way holds end |
| The rush | A waiting room admitting fans at payment speed | Autoscaling and hoping |
| Payments | Idempotency keys and automatic refunds when seats are gone | Charging again on every retry |
What the left column has in common: every choice points back to a requirement or a number from earlier. If you can say where each box came from, the interviewer will notice.
Stay in the loop
Get new problems and walkthroughs by email
One short email when something new is published: a deep-dive walkthrough, an LLD problem, or a real outage retold. No spam, unsubscribe any time.