Want it harder? Hide the model answer and work each phase in a scratchpad against its time budget, then reveal and score yourself. Or simply read on: the checkpoints below work either way.
How Do You Let Thirteen Million People Pick Seats for a Hundred and Fifty Thousand Without Selling Any Seat Twice?
It is 11:58 AM on a Sunday. A stadium concert goes on sale at noon. Across India, millions of phones and laptops sit on the same page, each one refreshing a little faster than the last. Only about one in ninety of these people can possibly get a ticket.
At the same moment, far from the stadium, a family in Pune is choosing four seats together in row H for the evening show of a new film. Two other people are looking at the same seat map. One of them taps H7 and H8 a second before the family does.
Both stories are the same problem at different volumes: a seat is a physical thing that can belong to exactly one person, people want to choose it themselves, and everyone wants it at once.
- 200 million — tickets a year at its pre-COVID annual run rate (AWS case study, January 2023)
- 2.3 million — tickets sold in 24 hours on 1 November 2024, a platform record (BookMyShow 2024 report)
- 6.8 million — tickets sold over the Dussehra weekend, 2025's highest-footfall weekend (BookMyShow 2025 report)
- 34,000 — live events hosted across India in 2025, up from 30,687 events in 319 cities in 2024
- 1.3 crore (13 million) — people who logged in for the Coldplay Mumbai sale on 22 September 2024, for about 1.5 lakh tickets
- ~15 minutes — how late that sale started after the site buckled at noon; BookMyShow said it ran a queue and blocked "suspicious and malicious traffic"
- 4 — tickets per user allowed in that sale
For context:
BookMyShow sells cinema, concert, sports and theatre tickets in India and a few other countries
After the Coldplay sale it filed a police complaint over tickets resold on unauthorised sites
Since 22 September 2025, GST on cinema tickets is 5% up to ₹100 and 18% above it
Everything in this design that is not in the list above is an illustrative design target, not BookMyShow's internal architecture
This is the system we'll design today.
About 1.3 crore people logged in for ~1.5 lakh Coldplay tickets, at most 4 each: only ~19 checkouts a second complete, while naive polling would cost 1.3 million requests a second, so waiting must be served from the edge. A stale read plane serves seat maps; one writer per show holds a booking's seats all together or not at all, with an 8-minute expiry; chain-owned seats are held through adapters that treat timeouts as unknown. The deep dives cover seat holds, the partner boundary, the on-sale waiting room and the live seat map, then blockbuster Friday, late payments and an on-sale on the same day as a blockbuster.
The Interview Begins#
You're interviewing at a company that sells tickets for cinemas, concerts and sports. The engineering director opens two tabs: a cinema seat map with a few seats greyed out, and a stadium plan coloured by price.
"Two products, one platform. Every day we sell cinema seats across thousands of screens: the customer picks the exact seats. A few times a year we put a stadium concert on sale and half the country turns up at noon. Design the booking system for both."
Customers browse shows, open a seat map, pick seats, pay, and get a ticket. Cinema chains, theatres and event organisers list their shows and inventory on the platform. Some inventory is owned by partners' own systems; some lives on the platform.
Requirements
- A seat is sold to exactly one person, ever
- A customer who picks seats gets them all, or none of them, never three of four
- Seats held during payment are released automatically if the payment doesn't finish
- Every captured rupee ends in a ticket or a refund, without a support ticket
- A stadium on-sale for 13 million people must not take down cinema booking
- Fair access: scripts and resellers must not beat humans to the seats
"Everyone starts with 'lock the row'. I want to hear where the seats actually live, how long you hold them, and what you do with the twelve million people who won't get one."
Phase 1: Requirements Clarification (5 minutes)#
"Before drawing anything, I want to know who owns a seat, and how long a customer is allowed to hold one."
Your Questions#
"Where does the seat inventory live? Do we own the cinema's seats, or does the cinema?"
"Assume both cases. Large cinema chains run their own ticketing systems, because they also sell at the box office and on their own apps. For those, the chain's system is the source of truth and we hold, confirm and release seats through their API. Smaller cinemas, theatres and most live events keep their inventory on our platform, so we are the source of truth."
"So the same seat can be sold by us, by the chain's app and by the box office at the same moment?"
"Yes. That's the point of asking the chain's system for a hold instead of trusting our own copy of the seat map."
"How long can a customer hold seats while paying?"
"Long enough to pay by UPI or card, short enough that abandoned holds don't empty a popular show on paper. Pick a number and defend it."
"What does a big on-sale look like?"
"When Coldplay's Mumbai dates went on sale in September 2024, about 1.3 crore people logged in for roughly 1.5 lakh tickets, at most 4 per person, and they sold out in under an hour. The site struggled at noon and the sale effectively started around 12:15. Assume the next one will be at least that big."
"Do stadium buyers pick exact seats, like in a cinema?"
"Some sections are seated and some are standing zones. For seated blocks, assume customers can either pick seats or ask for the best available in a price category. Standing zones are just a count."
"And resellers?"
"After that sale, tickets appeared on unauthorised resale sites within hours, and BookMyShow filed a police complaint. Assume adversarial traffic: scripts, account farms, and people buying to resell."
Functional Requirements#
1. Discovery
- Browse films, events and venues by city; show times and prices per show
- Seat map per show: layout, price categories, which seats are free right now
2. Seat selection and hold
- Pick specific seats, or "best available N" in a category
- Hold all selected seats atomically, with a visible countdown
- Release holds on timeout, cancel, or app close
3. Checkout
- Price the order: ticket price, convenience fee, taxes (GST 5% up to ₹100, 18% above, for cinema tickets)
- Pay by UPI, card, net banking or wallet
- Confirm the booking with the inventory owner (us or the cinema chain), then issue a ticket (QR code)
4. On-sales for big events
- A virtual waiting room that opens at a fixed time
- Per-account ticket limits (e.g. 4) across tabs, devices and retries
- Bot and reseller defences
5. After the sale
- Cancellations and refunds where the organiser allows them
- Entry scanning at the venue; a ticket can be used once
Non-Functional Requirements#
Scale
- Cinema: a peak day of ~3 million tickets (design target, rounded up from the 2.3 million record)
- On-sale: 13 million people waiting for ~150,000 tickets, sold out within an hour
Correctness
- Zero double-sold seats, on our inventory and on partners'
- Holds are all-or-nothing per booking
- Every captured payment ends in a ticket or an automatic refund
Latency
- Seat map open: < 300 ms p99
- Seat hold decision: < 500 ms p99 on our inventory; partner holds bounded by a 3 s timeout
- Queue status for a waiting customer: served from the edge, never from the booking core
Isolation
- An on-sale can never slow cinema booking
- One cinema chain's slow API can never slow another chain or our own inventory
Fairness
- One queue position per account, however many tabs are open
- Arrival order before the sale opens doesn't matter; arrival order after it does
Phase 2: Back of the Envelope Estimation (5 minutes)#
"Two very different load shapes share this platform: a steady daily cinema business, and a few on-sales that each look like a denial-of-service attack by fans."
Before the numbers: what really costs money in an on-sale?
13 million people wait for ~150,000 tickets, at most 4 per person, and the sale lasts under an hour. Roughly how many people can actually buy? If every waiting browser asked "is it my turn?" every 10 seconds, how many requests a second would that be, and what does it tell you about the design?
One strong answer
- Buyers: at an average of 3 tickets each, 150,000 tickets is about 50,000 buyers: one in 260 people in the queue.
- Checkout rate: 50,000 buyers in ~45 minutes is only about 19 completed bookings a second. The write path is small.
- Polling: 13,000,000 ÷ 10 s = 1.3 million requests a second just to say "not yet".
So the expensive part of an on-sale is the waiting, not the booking. The waiting room has to answer "not yet" from the edge (signed tokens, cached status, polling intervals that grow with your position) so that the booking core only ever sees the few thousand people actually choosing seats.
Traffic Estimation: The Daily Cinema Business#
- Public baseline: up to 200 million tickets a year pre-COVID = ~550,000 a day on average; record day 2.3 million
- Design target for a peak day: 3 million tickets
- Average booking: 2.5 tickets → 1.2 million bookings a day
- Busiest hour, assumed to carry 12% of the day: 144,000 bookings → ~40 bookings a second
- Blockbuster advance booking opening (assumed 5× the busiest hour for an hour): ~200 bookings a second
- Seat map opens, assuming ~15 per completed booking (people compare shows and seats): 40 × 15 = ~600 a second, ~3,000 a second when a blockbuster opens
- Hold attempts, assuming 1.3 per booking (some lose a race and retry): ~50 a second, ~260 at the blockbuster peak
Conclusion: the cinema write path is a few hundred operations a second. The hard part is not throughput but contention: a few popular shows where many people want the same rows at the same moment.
Traffic Estimation: The On-Sale#
- People in the waiting room: 13,000,000
- Tickets: ~150,000, limit 4 a person
- Buyers, at ~3 tickets each: ~50,000
- Sale duration: ~45 minutes
- Completed checkouts: 50,000 ÷ 2,700 s = ~19 a second
- Average time in checkout (pick seats, pay by UPI): ~3 minutes, so ~3,300 people choosing and paying at once
- Waiting-room status checks if every client polled every 10 s: 1.3 million a second
- With polling every 60 s for anyone more than 50,000 places back (almost everyone): ~220,000 a second, served from cache at the edge
Conclusion: admit people into checkout at about the rate tickets can be sold (tens a second), and make waiting nearly free.
Storage Estimation#
- Screens with listed shows (design assumption): 10,000
- Shows a screen a day: 5; bookable days ahead: 7 → 350,000 open shows
- Seats a show: ~250 on average → ~88 million seat states
- One seat's state (free / held / sold) in 2 bits: one show's map is ~63 bytes; all open shows: ~22 MB
- A stadium of 60,000 seats: 15 KB of state
- Bookings: 1.2 million a day × ~1.5 KB (seats, prices, taxes, payment and partner references, states) = ~1.8 GB a day, ~650 GB a year
- Hold records: short-lived, a few hundred thousand at any moment; they expire in minutes
Conclusion: every live seat map in the country fits in memory on one machine. The design problem is concurrency and ownership, not space.
Key Metrics Summary#
| Metric | Value |
|---|---|
| Peak day (design target) | 3 million tickets, 1.2 million bookings |
| Bookings, busiest hour / blockbuster opening | ~40/s / ~200/s |
| Seat map opens, busiest hour / blockbuster | ~600/s / ~3,000/s |
| On-sale: waiting / buyers | 13 million / ~50,000 |
| On-sale: completed checkouts | ~19/s |
| On-sale: naive polling load | 1.3 million req/s |
| All open seat maps in memory | ~22 MB |
Requirements and estimation
Q1/3 13 million people wait for about 150,000 tickets and buyers take about 3 tickets each. Roughly how many people can buy?
Answer & explanation
Answer: B
150,000 ÷ 3 = 50,000 buyers: roughly one person in 260. The other 12.95 million must be told, honestly and cheaply, that they won't get in.
Q2/3 Why is "1.3 million requests a second" the most important number in the on-sale estimate?
Answer & explanation
Answer: C
The sale itself completes ~19 bookings a second. If "is it my turn?" reaches the core, the waiting crowd takes the system down before anyone buys.
Q3/3 Every open seat map in the country takes about 22 MB at 2 bits a seat. What does that tell you?
Answer & explanation
Answer: B
Seat state is tiny. The hard questions are who is allowed to change a seat, how to change several seats atomically, and what to do when the owner is a partner's system.
Phase 3: High-Level Design (10 minutes)#
Sketch it first: from seat map to ticket
Trace one booking for four seats: the customer opens the seat map, taps H5 to H8, pays, and gets a QR code. Which steps can use slightly stale data, which must be exact, and what changes when the show belongs to a cinema chain that runs its own ticketing system?
One strong answer
- Stale is fine: listings, show times, prices and the seat map's free/taken colours, served from a cache a second or two old and updated by pushes. A stale colour costs one failed tap.
- Must be exact: the hold. All four seats are held in one atomic step against the inventory owner, with an expiry. If any seat is taken, nothing is held and the customer picks again.
- Partner inventory: the hold goes through an adapter to the chain's system, with a timeout, an idempotency key and an "unknown" outcome. Our copy of their seat map is only a display cache.
- Payment: a booking saga takes the money against the held seats, confirms with the owner, issues the ticket, or releases the seats and refunds.
- On-sales: a separate waiting room in front of the same booking path, admitting people at the rate tickets can sell, on capacity that cinema traffic doesn't share.
"I'll split the platform the way the load splits. A read plane serves listings and seat maps to everyone, from caches. A small, exact write plane owns holds, bookings and payments. Inventory sits behind one interface with two implementations: our own seat store, and adapters to partners' systems. And on-sales get their own front door."
System Architecture#
%% title: Seat-booking platform architecture
flowchart TD
subgraph L1["Layer 1 · Clients"]
direction LR
APP@{ icon: "sd:phone", label: "Customer app" }
WEB@{ icon: "sd:browser", label: "Website" }
GATE@{ icon: "sd:scan", label: "Venue entry scanners" }
end
subgraph L2["Layer 2 · Edge"]
direction LR
CDN@{ icon: "sd:cdn", label: "CDN<br/>posters, seat layouts" }
WR["Waiting room<br/>on-sales only, signed queue tokens"]:::queue
GW@{ icon: "sd:gateway", label: "API gateway<br/>auth, rate limits, bot scores" }
end
subgraph L3["Layer 3 · Read plane (seconds stale is fine)"]
direction LR
CAT["Catalog + showtimes"]:::service
MAP@{ icon: "sd:cache", label: "Seat map cache<br/>bitmap per show" }
PUSH@{ icon: "sd:stream", label: "Seat change stream<br/>per-show channel" }
end
subgraph L4["Layer 4 · Write plane (exact)"]
direction LR
HOLD["Hold service<br/>all-or-nothing, TTL"]:::hot
BK["Booking saga<br/>hold → pay → confirm → ticket"]:::core
PAY["Payment service<br/>idempotency key"]:::service
end
subgraph L5["Layer 5 · Inventory owners"]
direction LR
SEAT@{ icon: "sd:database", label: "Seat store<br/>our inventory, sharded by show" }
ADP["Partner adapters<br/>one per cinema chain"]:::service
POS@{ icon: "sd:server", label: "Chain ticketing systems<br/>(box office, own apps)" }
end
PG@{ icon: "sd:card", label: "Payment gateways" }
L1 ==> L2
CDN --> L3
GW --> L3
WR ==> GW
GW ==>|"hold, book"| L4
HOLD ==> SEAT
HOLD ==> ADP --> POS
PAY --> PG
SEAT -.->|"changes"| PUSH
class APP,WEB,GATE client
class CDN edge
class GW edge
class MAP cache
class PUSH queue
class SEAT store
class POS,PG externalKEY INSIGHTS:
- The seat map cache is a projection. It never decides anything; the hold service does, against the owner of the seat
- The hold service is the only writer of seat state on our inventory, and the only caller of partners' hold APIs. One interface, two kinds of owner
- The waiting room exists only during on-sales, on its own capacity. When there is no on-sale it passes everyone straight through
Data Flow: Four Seats in Row H#
"Let's follow Ananya booking H5 to H8 for the 7:15 PM show, on our own inventory first."
%% title: One booking, seat map to ticket sequenceDiagram autonumber participant A as Ananya's app participant R as Read plane participant H as Hold service participant B as Booking saga participant P as Payment A->>R: Open seat map, show 7:15 PM R-->>A: Layout from CDN + bitmap (version 418) A->>H: Hold H5, H6, H7, H8 (key k1) H-->>A: Held, all 4, expires in 8 min (hold h1) A->>B: Checkout h1 (key k1) B->>P: Create payment, ₹1,240 (key k1) P-->>B: UPI success B->>H: Convert hold h1 to sold H-->>B: Sold, version 419 B-->>A: Booking confirmed, QR ticket Note over R: Version 419 pushed to everyone viewing the show: H5-H8 turn grey
MEANWHILE, IN PARALLEL:
- Two other people looking at the same show saw H5 to H8 turn "held" within a second of step 4, and stopped tapping them
- If Ananya closes the app at step 6, the hold simply expires after 8 minutes and the seats come back
- If the show belonged to a cinema chain, step 4 would go through the chain's adapter, and step 9 would be a "confirm" call to the chain's system
High-level design
Q1/3 The seat map showed H7 as free, but Ananya's hold for H5 to H8 fails because H7 was held a second earlier. What should happen?
Answer & explanation
Answer: B
Holds are all-or-nothing: a family wants four seats together, not three plus a stray. The map was a second stale, which is fine; the hold is the exact check.
Q2/3 Why does the hold for a cinema chain's show go to the chain's system instead of our own copy of its seat map?
Answer & explanation
Answer: C
Whoever sells a seat from more than one place must have one source of truth. For chain-owned inventory that is the chain's system; our copy is a display cache.
Q3/3 Why does the on-sale waiting room sit on capacity that cinema traffic doesn't share?
Answer & explanation
Answer: C
Isolation is a requirement. The on-sale's load is mostly waiting; it must be absorbed where it can't touch the paths that daily bookings use.
Phase 4: Deep Dives (20 minutes)#
Deep Dive 1: The Seat Hold, Many Seats in One Atomic Decision (Week 5 Concepts: Consistency Models, Week 2: Idempotency)#
"Tatkal is one counter and hundreds of identical seats. Here every seat is different and people choose them, so the contention is per seat, and a booking wants several seats at once."
Four seats, two families, one second
Family A taps H5 to H8. Family B taps H7 to H10 at the same moment. How do you make sure exactly one family gets H7 and H8, that neither ends up with a partial set, and that seats held by someone who walked away come back without anyone cleaning up?
One strong answer
- One owner per show: all seat changes for a show go through one place (one shard of the seat store, or one in-memory owner for that show), so the race is decided there in one step.
- All or nothing: a single atomic operation checks every requested seat is free and, only if all are, marks them all held with the same hold id. Family A's request runs first and wins H5 to H8; family B's finds H7 taken and holds nothing.
- Expiry built in: each held seat carries its hold id and an expiry time. A seat whose hold has expired counts as free at the next check, and a sweeper tidies up. Nobody has to remember to release it.
- Idempotent retries: the hold request carries the client's key. A retried tap returns the same hold instead of holding a second set.
- Release and sell by hold id: converting to sold or releasing only touches seats that still carry this hold id, so an expired hold can never sell seats someone else now holds.
The Challenge:
1. Seat-by-seat locking
Lock H5, then H6, then H7... Two families locking overlapping rows in different orders deadlock, and one that fails halfway leaves seats locked that nobody is buying.
2. Check, then write
Read "H7 is free", then write "H7 held". Between the two, someone else did the same. Both customers think they have H7.
3. Holds that never end
A customer opens the payment page and puts the phone down. Without an expiry, H5 to H8 stay blocked for the evening while the show is "sold out" on paper.
The Solution:
%% title: Life of one seat stateDiagram-v2 [*] --> FREE FREE --> HELD: hold accepted (all seats or none) HELD --> SOLD: payment confirmed, same hold id HELD --> FREE: released, or hold expired SOLD --> REFUNDED_FREE: cancelled where allowed REFUNDED_FREE --> FREE SOLD --> USED: scanned at the venue FREE: FREE HELD: HELD · hold id + expiry (8 min) SOLD: SOLD · booking id REFUNDED_FREE: back on sale USED: USED · scanned once class FREE ok class HELD queue class SOLD,USED core class REFUNDED_FREE warn
KEY DESIGN DECISIONS:
- ONE WRITER PER SHOW — Seat state is partitioned by show. Every hold, sale and release for a show lands on the same shard, so there is no cross-machine lock, and one blockbuster's rush touches one shard.
- ONE ATOMIC CHECK-AND-SET FOR ALL SEATS — In memory (a script over the show's seats) or in SQL (a conditional update of N rows that must affect exactly N rows, otherwise roll back). Either way, all seats change together or none do.
- THE EXPIRY LIVES ON THE SEAT — A held seat stores
hold_idandexpires_at. The check treats an expired hold as free, so correctness never depends on the sweeper running on time. - HOLD FOR 8 MINUTES — Long enough to pay by UPI or card with an OTP; short enough that abandoned holds come back during the same browsing session. On-sales use a shorter hold (say 5 minutes), because the queue behind is long.
- THE HOLD ID IS THE CAPABILITY — Selling or releasing requires the hold id. A late payment can't sell seats that have since been held by someone else.
# inventory/seat_holds.py
"""
All-or-nothing seat holds for shows on our own inventory.
Seat state for one show lives in one hash on one shard:
show:{show_id}:seats field = seat id ("H7"), value = state string
"F" free
"H|{hold_id}|{expires}" held until expires (epoch ms)
"S|{booking_id}" sold
Every operation on a show is one Lua script, so it runs atomically
on that shard: no seat-by-seat locks, no deadlocks, no partial holds.
"""
import time
import uuid
from dataclasses import dataclass
from typing import List, Optional
HOLD_SCRIPT = """
-- KEYS[1] = show seats hash, KEYS[2] = idempotency key -> hold id
-- ARGV[1] = now (ms), ARGV[2] = hold id, ARGV[3] = expiry (ms), ARGV[4..] = seat ids
local existing = redis.call('GET', KEYS[2])
if existing then return {'SAME', existing} end -- retried tap: same hold
local now = tonumber(ARGV[1])
local taken = {}
for i = 4, #ARGV do
local v = redis.call('HGET', KEYS[1], ARGV[i])
if v == false then return {'NO_SUCH_SEAT', ARGV[i]} end
if string.sub(v, 1, 1) == 'S' then
table.insert(taken, ARGV[i])
elseif string.sub(v, 1, 1) == 'H' then
local exp = tonumber(string.match(v, '|(%d+)$'))
if exp > now then table.insert(taken, ARGV[i]) end -- live hold
end
end
if #taken > 0 then return {'TAKEN', unpack(taken)} end -- hold nothing
local val = 'H|' .. ARGV[2] .. '|' .. ARGV[3]
for i = 4, #ARGV do redis.call('HSET', KEYS[1], ARGV[i], val) end
redis.call('SET', KEYS[2], ARGV[2], 'PX', tonumber(ARGV[3]) - now + 60000)
redis.call('INCR', KEYS[1] .. ':version')
return {'HELD', ARGV[2]}
"""
SELL_SCRIPT = """
-- KEYS[1] = show seats hash; ARGV[1] = hold id, ARGV[2] = booking id,
-- ARGV[3] = now (ms), ARGV[4..] = seat ids
local now = tonumber(ARGV[3])
for i = 4, #ARGV do
local v = redis.call('HGET', KEYS[1], ARGV[i])
local hid, exp = string.match(v or '', '^H|([^|]+)|(%d+)$')
-- the seat must still carry OUR hold, and it must not have expired
if hid ~= ARGV[1] or tonumber(exp) <= now then return {'LOST', ARGV[i]} end
end
for i = 4, #ARGV do redis.call('HSET', KEYS[1], ARGV[i], 'S|' .. ARGV[2]) end
redis.call('INCR', KEYS[1] .. ':version')
return {'SOLD'}
"""
@dataclass
class HoldResult:
status: str # HELD | SAME | TAKEN | NO_SUCH_SEAT
hold_id: Optional[str] = None
taken: Optional[List[str]] = None
class SeatHoldService:
HOLD_MS = 8 * 60 * 1000 # cinema; on-sales pass a shorter hold
def __init__(self, redis_for_show, durable_log):
self.redis_for_show = redis_for_show # show_id -> the shard that owns it
self.log = durable_log # append-only record of every change
def hold(self, show_id: str, seats: List[str], idem_key: str, hold_ms: int = HOLD_MS) -> HoldResult:
r = self.redis_for_show(show_id)
now = int(time.time() * 1000)
hold_id = str(uuid.uuid4())
res = r.eval(HOLD_SCRIPT, 2, f"show:{show_id}:seats", f"holdkey:{idem_key}",
now, hold_id, now + hold_ms, *sorted(seats))
status = res[0]
if status in ("HELD", "SAME"):
if status == "HELD":
self.log.append("hold", show_id=show_id, hold_id=hold_id, seats=seats, expires=now + hold_ms)
return HoldResult(status, hold_id=res[1])
return HoldResult(status, taken=list(res[1:]))
def sell(self, show_id: str, hold_id: str, seats: List[str], booking_id: str) -> bool:
"""Payment confirmed. False if the hold expired and any seat moved on."""
r = self.redis_for_show(show_id)
now = int(time.time() * 1000)
res = r.eval(SELL_SCRIPT, 1, f"show:{show_id}:seats", hold_id, booking_id, now, *sorted(seats))
if res[0] == "SOLD":
self.log.append("sold", show_id=show_id, hold_id=hold_id, booking_id=booking_id, seats=seats)
return True
return FalseA few details carry the correctness:
- The durable log is the system of record. The in-memory hash is rebuilt from it after a failover, and every sale is written there before the ticket is issued; a sale the log doesn't have never happened.
sorted(seats)costs nothing here (the script is atomic anyway) but keeps the log and the SQL variant (below) deadlock-free.- The same idea in SQL, if the show lives in a relational database:
UPDATE seats SET state='HELD', hold_id=?, expires_at=? WHERE show_id=? AND seat_id IN (...) AND (state='FREE' OR (state='HELD' AND expires_at < now())), then commit only if the row count equals the number of seats requested. - Best available for stadium categories is the same hold with the seat list chosen by the server: the best contiguous block of N free seats in the category, retried once if it loses a race.
Deep Dive 2: Seats You Don't Own, Holding Inventory in a Cinema Chain's System (Week 2 Concepts: Timeouts and Circuit Breakers, Week 5: Saga)#
"For a chain-owned show, our hold service doesn't decide anything. It asks the chain's system, over the network, under a timeout. Every rule from Deep Dive 1 still has to hold, but now across a boundary we don't control."
The partner didn't answer
We send "hold H5 to H8" to a cinema chain's system and get no reply within 3 seconds. Did the hold happen? What do you tell the customer, what do you send next, and what happens if the chain's system is down for ten minutes on a Friday evening?
One strong answer
- Unknown is a real outcome: a timeout means "maybe held". Don't retry blindly with a new request (you might hold eight seats), and don't assume failure (you might leave four seats locked at the chain).
- Idempotent requests: every call carries our hold id as the partner's reference. A retry with the same id either returns the existing hold or creates it once. If the partner's API has no such idea, follow up with "status of hold h1" before anything else.
- Tell the customer the truth: "still checking these seats" for a few seconds, then either the hold or "please pick again". Never show the payment page for a hold we aren't sure of.
- Clean up on our side: if we gave up on a hold that might exist, send a release for that hold id; partner holds also expire on their own clock.
- Ten minutes down: a circuit breaker per chain stops sending holds, the chain's shows say "booking temporarily unavailable", and every other chain and our own inventory carry on. Seat maps for that chain are marked stale.
The Challenge:
- The partner's hold API can be slow, fail, or succeed without our hearing about it
- The partner's hold expiry and ours can disagree by seconds or minutes
- The box office sells seats our cached map still shows as free
- "Confirm" can time out after the customer has paid
- One chain's outage on a Friday night must not touch anyone else's bookings
The Solution:
%% title: Hold, pay, confirm across a partner boundary sequenceDiagram autonumber participant A as App participant H as Hold service participant D as Chain adapter participant C as Chain system participant B as Booking saga A->>H: Hold H5-H8 (key k1) H->>D: hold(ref=h1, seats, ttl 8 min) D->>C: POST /holds ref h1 C-->>D: (no reply in 3 s) D->>C: GET /holds/h1 C-->>D: Held until 19:03 D-->>H: Held (partner expiry 19:03) H-->>A: Held, pay by 19:02 A->>B: Pay B->>D: confirm(ref=h1, booking b9) D->>C: POST /holds/h1/confirm C-->>D: Confirmed, booking C-55120 D-->>B: Confirmed B-->>A: Ticket
KEY DESIGN DECISIONS:
- OUR HOLD ID IS THE PARTNER'S REFERENCE — Every hold, status, confirm and release call carries it. That turns "retry" from dangerous into safe.
- SHOW THE CUSTOMER THE EARLIER EXPIRY — Our countdown ends a minute before the partner's, so the payment can't succeed against a hold the partner already dropped.
- CONFIRM IS RETRIED, NEVER GUESSED — If confirm times out after payment, the booking goes to
CONFIRM_PENDING, the adapter asks for the hold's status until it gets an answer, and only a definite "gone" leads to a refund. - A BULKHEAD PER CHAIN — Separate connection pools, timeouts and circuit breakers per partner. A slow chain fills its own pool, not everyone's.
- OUR MAP OF THEIR SEATS IS A HINT — Refreshed from the partner's seat-layout API every few seconds for open seat maps, and corrected whenever a hold says "taken".
# partners/chain_adapter.py
"""
Adapter between our hold service and one cinema chain's ticketing
system. Every request is idempotent on our hold id; every timeout is
treated as UNKNOWN and resolved with a status call, never guessed.
"""
from dataclasses import dataclass
from enum import Enum
from typing import List, Optional
class Outcome(Enum):
HELD = "held"
TAKEN = "taken"
CONFIRMED = "confirmed"
GONE = "gone" # hold expired or released at the partner
UNKNOWN = "unknown" # no definite answer yet
@dataclass
class PartnerReply:
outcome: Outcome
partner_ref: Optional[str] = None
expires_at_ms: Optional[int] = None
class CircuitOpen(Exception):
pass
class ChainAdapter:
TIMEOUT_S = 3.0
STATUS_TRIES = 3
def __init__(self, chain_id: str, http, breaker, metrics):
self.chain_id = chain_id
self.http = http # pooled client, one pool per chain (bulkhead)
self.breaker = breaker # opens after repeated timeouts or 5xx
self.metrics = metrics
def hold(self, hold_id: str, show_ref: str, seats: List[str], ttl_s: int) -> PartnerReply:
if self.breaker.is_open():
raise CircuitOpen(self.chain_id)
try:
r = self.http.post("/holds", timeout=self.TIMEOUT_S,
json={"ref": hold_id, "show": show_ref, "seats": seats, "ttl": ttl_s})
self.breaker.record_success()
if r.status == 409:
return PartnerReply(Outcome.TAKEN)
return PartnerReply(Outcome.HELD, r.json()["id"], r.json()["expires_at_ms"])
except TimeoutError:
self.breaker.record_failure()
return self.status(hold_id) # did it happen? ask, don't guess
def status(self, hold_id: str) -> PartnerReply:
for _ in range(self.STATUS_TRIES):
try:
r = self.http.get(f"/holds/{hold_id}", timeout=self.TIMEOUT_S)
if r.status == 404:
return PartnerReply(Outcome.GONE)
body = r.json()
state = {"HELD": Outcome.HELD, "CONFIRMED": Outcome.CONFIRMED}.get(body["state"], Outcome.GONE)
return PartnerReply(state, body.get("id"), body.get("expires_at_ms"))
except TimeoutError:
self.metrics.increment("partner_status_timeout", self.chain_id)
return PartnerReply(Outcome.UNKNOWN)
def confirm(self, hold_id: str, booking_id: str) -> PartnerReply:
try:
r = self.http.post(f"/holds/{hold_id}/confirm", timeout=self.TIMEOUT_S,
json={"booking": booking_id})
if r.status == 410:
return PartnerReply(Outcome.GONE)
return PartnerReply(Outcome.CONFIRMED, r.json()["booking_ref"])
except TimeoutError:
# Confirm is safe to repeat with the same ref; the saga will ask again.
return self.status(hold_id)
def release(self, hold_id: str) -> None:
"""Best effort: the partner's own expiry is the backstop."""
try:
self.http.post(f"/holds/{hold_id}/release", timeout=self.TIMEOUT_S)
except TimeoutError:
self.metrics.increment("partner_release_timeout", self.chain_id)Seat holds and partner inventory
Q1/4 Two families request overlapping seats (H5-H8 and H7-H10) at the same instant on one show. What does the hold script guarantee?
Answer & explanation
Answer: C
The script checks every seat and writes all of them, or none, in one atomic step on the show's shard. The loser is told which seats were taken and picks again.
Q2/4 A held seat stores its expiry. Why does the hold check treat an expired hold as free, instead of relying on a sweeper to release it?
Answer & explanation
Answer: B
The sweeper only tidies up. If it runs late, the next hold attempt still sees the seat as free, so abandoned holds can't block a show.
Q3/4 A hold request to a cinema chain times out after 3 seconds. What should the adapter do next?
Answer & explanation
Answer: C
A timeout is "unknown". A fresh request could hold a second set; assuming success could sell seats we don't have. The status call, keyed on our hold id, turns unknown into a fact.
Q4/4 One cinema chain's system goes down on Friday evening. What stops it from hurting every other booking?
Answer & explanation
Answer: D
Separate pools, timeouts and breakers per partner keep one chain's failure inside its own compartment. Its shows say "temporarily unavailable"; everything else carries on.
Deep Dive 3: The On-Sale, Thirteen Million People and One Front Door (Week 3 Concepts: Backpressure, Week 4: Thundering Herd, Week 9: Security)#
"Seat holds handle a few hundred people fighting over a row. An on-sale is a different animal: millions of people, most of whom will never touch a seat, all arriving in the same second."
Who gets in, in what order, and how fast?
At 12:00:00 the sale opens. 13 million people are already on the page, and more arrive every second. How do you order them fairly, how fast do you let them into seat selection, and when do you tell the people at the back that it's over?
One strong answer
- Before noon, arrival doesn't matter: everyone who joined the waiting room before the sale opened gets a random position at 12:00:00. Refreshing at 11:59:59.900 earns nothing, so scripts gain nothing by hammering.
- After noon, it does: people who arrive later go to the back, in arrival order.
- One position per account: a queue token is bound to the account (and device). Ten tabs or a retry get the same position, not ten.
- Admit at the rate tickets can sell: start with the checkout capacity (say ~3,000 people choosing and paying at once) and admit as people finish, not on a fixed timer. Roughly 20 a second here.
- Stop admitting early: when tickets left minus tickets in active holds can no longer serve the next person, stop admitting, keep the queue waiting a few minutes for expired holds, then tell everyone "sold out" at once.
- Make waiting free: the status page is static, positions are signed tokens checked at the edge, and polling slows down the further back you are.
The Challenge:
- A step change from almost nothing to millions of requests in one second; autoscaling can't react that fast
- Everyone refreshing makes it worse: each refresh is another request, and a lost position
- Scripts open hundreds of sessions, and account farms multiply the per-person limit
- People who will never get a ticket keep the system busy for an hour, unless they are told early and honestly
- The ticketing core, if exposed, sees the whole crowd; in September 2024 the Coldplay sale started about 15 minutes late after the site buckled at noon
The Solution:
%% title: The on-sale front door
flowchart TD
U@{ icon: "sd:users", label: "13 million people" }
EDGE@{ icon: "sd:cdn", label: "Edge<br/>static waiting page, cached status" }
BOT@{ icon: "sd:shield", label: "Bot checks<br/>device, account age, velocity" }
subgraph WR["Waiting room"]
direction TB
LOT["12:00:00 · random order for everyone already waiting"]:::queue
LATE["After 12:00 · later arrivals join the back"]:::queue
TOK["Signed token: account, position, sale id"]:::service
end
ADM{"Room in checkout?<br/>and tickets left after active holds?"}:::cache
CK["Seat selection + checkout<br/>~3,000 at once, 5-min holds"]:::hot
SO(["Sold out, said once, to everyone"]):::bad
U ==> EDGE ==> BOT ==> LOT
LOT --> TOK
LATE --> TOK
TOK ==> ADM
ADM ==>|"yes"| CK
ADM -->|"tickets gone"| SO
CK -.->|"finished or expired: free a slot"| ADM
class U client
class EDGE edge
class BOT edgeKEY DESIGN DECISIONS:
- A LOTTERY AT THE OPEN, A LINE AFTER IT — Pre-sale arrivals are shuffled at 12:00:00; post-sale arrivals queue in order. Fair to humans, worthless to scripts that race the clock.
- ADMIT ON COMPLETIONS, NOT ON A TIMER — The waiting room keeps a target number of people in checkout. Every finished or expired checkout frees one slot. The admit rate follows what the booking core actually absorbs.
- THE TOKEN IS THE PASS — A signed token carries the account, the position and the sale. The edge verifies it without a database lookup; the checkout service accepts only admitted tokens; a token can't be shared because it is bound to the account.
- LIMITS COUNT EVERYTHING — The 4-ticket limit is enforced at hold time against tickets held plus tickets bought by that account, across every tab and retry.
- SAY "SOLD OUT" EARLY AND ONCE — When remaining tickets minus active holds is zero, stop admitting; wait out the active holds (5 minutes); then release the leftovers to the next people in line, or close the queue for everyone with one message.
# onsale/waiting_room.py
"""
Waiting room for one on-sale. Positions are drawn by lottery for
everyone present at the open and assigned in arrival order after it.
People are admitted only when a checkout slot frees up and tickets
remain beyond what active holds may still sell.
"""
import hashlib
import hmac
import json
import time
from dataclasses import dataclass
@dataclass
class SaleState:
tickets_left: int # unsold, not counting active holds
held_now: int # tickets in active holds (may come back)
in_checkout: int # people currently choosing or paying
target_in_checkout: int # e.g. 3,000
class WaitingRoom:
def __init__(self, sale_id: str, opens_at: float, secret: bytes, store, accounts):
self.sale_id = sale_id
self.opens_at = opens_at
self.secret = secret
self.store = store # sorted set of positions, per sale
self.accounts = accounts # one position per account
def join(self, account_id: str, now: float) -> dict:
existing = self.accounts.get(self.sale_id, account_id)
if existing:
return existing # same position for every tab and retry
if now < self.opens_at:
# Before the open: a stable random rank from a secret, revealed at 12:00:00.
# Joining at 11:59:59.900 is worth exactly as much as joining at 11:30.
rank = int.from_bytes(hmac.new(self.secret, f"{self.sale_id}:{account_id}".encode(),
hashlib.sha256).digest()[:8], "big")
score = rank / 2**64 # 0 <= score < 1: ahead of every late joiner
else:
score = 1 + (now - self.opens_at) # after the open: arrival order
self.store.add(self.sale_id, account_id, score)
token = self._sign({"sale": self.sale_id, "acct": account_id, "score": score})
self.accounts.put(self.sale_id, account_id, token)
return token
def admit_tick(self, s: SaleState) -> int:
"""Runs every second. Returns how many people were admitted."""
if s.tickets_left <= 0:
if s.held_now == 0:
self.store.close(self.sale_id, message="SOLD_OUT") # one message, everyone
return 0 # wait out active holds
free_slots = max(0, s.target_in_checkout - s.in_checkout)
# Don't admit more people than the remaining tickets could possibly serve.
could_serve = s.tickets_left // 2 + 1 # assume ~2+ tickets per buyer
n = min(free_slots, could_serve)
for account_id in self.store.pop_lowest(self.sale_id, n):
self.accounts.mark_admitted(self.sale_id, account_id, expires_in_s=5 * 60)
return n
def poll_after_s(self, position: int) -> int:
"""Polling interval grows with distance from the front: waiting is nearly free."""
if position < 5_000:
return 5
if position < 50_000:
return 20
return 60
def _sign(self, body: dict) -> dict:
raw = json.dumps(body, sort_keys=True).encode()
return {**body, "sig": hmac.new(self.secret, raw, hashlib.sha256).hexdigest(), "iat": int(time.time())}"The bot side is layered like any abuse problem: verified phone numbers and account age to join, device and IP velocity checks at the edge, a step-up challenge for suspicious sessions, and the per-account limit at hold time. And the waiting room doesn't need to be perfect at catching bots, because the lottery means a bot that joins early gains nothing over a person who joined early."
The on-sale
Q1/4 Why give everyone who joined before 12:00:00 a random position, rather than first come, first served?
Answer & explanation
Answer: C
With pure arrival order, the fastest script wins the front of the line. With a lottery at the open, joining at 11:30 or at 11:59:59.900 is worth the same.
Q2/4 The waiting room admits people "on completions", keeping ~3,000 in checkout. What does that achieve?
Answer & explanation
Answer: A
A fixed rate is either too slow or too fast. Freeing a slot only when someone finishes or their hold expires keeps checkout full but never overloaded.
Q3/4 200 tickets remain, 1,800 tickets are in active holds, and 40,000 people are still waiting. What should the waiting room do?
Answer & explanation
Answer: D
Holds that expire put tickets back. Admit roughly what's left (about 100 buyers at 2+ tickets each), then wait out the holds before saying "sold out", once, to everyone.
Q4/4 A buyer opens five tabs and runs a script on a second phone with the same account. How many queue positions does the account get?
Answer & explanation
Answer: C
join() returns the existing position for an account. Tabs and retries change nothing, and the 4-ticket limit is checked at hold time across everything the account holds or has bought.
Deep Dive 4: The Live Seat Map (Week 4 Concepts: Caching, Week 8: Streaming)#
"The seat map is the product. If it lies, people lose races they didn't know they were in. If it's exact, it's expensive. The answer is a map that's at most a second or two behind, and honest about it when a tap loses."
Thousands of people looking at one seat map
A blockbuster's first-day evening show at a popular multiplex has 3,000 people looking at its seat map at once, and seats are being held every second. How do you send each of them the current map, and keep it current, without every viewer asking the seat store every second?
One strong answer
- Split layout from state: the layout (rows, gaps, seat names, price categories) never changes for a show; serve it from the CDN. The state is a bitmap: 2 bits a seat, a few hundred bytes.
- Version every change: each hold, sale or release bumps the show's version. A viewer's map is "version 418".
- Push deltas: one channel per show. Viewers subscribe and receive "v419: H5-H8 held". A viewer who misses a delta (gap in versions) fetches the full bitmap again.
- Coalesce: during a rush, send at most one delta per show every 500 ms, carrying all changes since the last. 3,000 viewers × 2 messages a second is 6,000 small messages, cheap for a fan-out tier.
- Be honest on a lost race: if a hold fails, show which seats were taken and refresh the map immediately. A second of staleness costs one tap, never a double sale.
The Challenge:
- 3,000 viewers polling every second is 3,000 reads a second on one show's shard, during exactly the minutes it is busiest selling
- Polling slower makes the map stale and people tap seats that are gone
- A full map for a 60,000-seat stadium sent on every change is 15 KB × viewers × changes
The Solution:
%% title: Seat state from the store to every viewer
flowchart TD
HS["Hold service<br/>every change bumps the version"]:::hot
LOG@{ icon: "sd:log", label: "Seat change log<br/>(show, version, seats, state)" }
CO["Coalescer<br/>≤ 1 delta per show per 500 ms"]:::service
FAN@{ icon: "tech:websocket", label: "Fan-out tier<br/>one channel per show" }
SNAP@{ icon: "sd:cache", label: "Snapshot cache<br/>full bitmap + version" }
V1@{ icon: "sd:phone", label: "Viewers (3,000)" }
LAY@{ icon: "sd:cdn", label: "Layout from CDN<br/>never changes" }
HS ==> LOG ==> CO ==> FAN ==> V1
LOG --> SNAP
V1 -.->|"gap in versions: refetch"| SNAP
LAY --> V1
class LOG queue
class FAN edge
class SNAP cache
class V1 client
class LAY edge# seatmap/deltas.py
"""
Turns the seat change log into versioned deltas per show, coalesced so
a busy show sends at most one message per window to its viewers.
"""
from collections import defaultdict
from typing import Dict, List, Tuple
FREE, HELD, SOLD = 0, 1, 2
class ShowBitmap:
"""2 bits per seat; seat index comes from the show's layout."""
def __init__(self, n_seats: int):
self.bits = bytearray((n_seats * 2 + 7) // 8)
self.version = 0
def set(self, idx: int, state: int) -> None:
byte, shift = divmod(idx * 2, 8)
self.bits[byte] = (self.bits[byte] & ~(0b11 << shift)) | (state << shift)
def get(self, idx: int) -> int:
byte, shift = divmod(idx * 2, 8)
return (self.bits[byte] >> shift) & 0b11
class Coalescer:
WINDOW_MS = 500
def __init__(self, fanout, snapshots):
self.fanout = fanout
self.snapshots = snapshots
self.pending: Dict[str, List[Tuple[int, int]]] = defaultdict(list)
self.from_version: Dict[str, int] = {}
def on_change(self, show_id: str, version: int, changes: List[Tuple[int, int]]) -> None:
bm = self.snapshots.get(show_id)
for idx, state in changes:
bm.set(idx, state)
bm.version = version
self.from_version.setdefault(show_id, version - 1)
self.pending[show_id].extend(changes)
def flush(self) -> None:
"""Called every WINDOW_MS: one message per changed show."""
for show_id, changes in self.pending.items():
last = {}
for idx, state in changes:
last[idx] = state # only the final state of each seat matters
self.fanout.publish(f"show:{show_id}", {
"from": self.from_version[show_id],
"to": self.snapshots.get(show_id).version,
"seats": sorted(last.items()),
})
self.pending.clear()
self.from_version.clear()
def apply_on_client(local_version: int, msg: dict) -> str:
"""Client side: apply a delta only if it continues our version."""
if msg["from"] != local_version:
return "REFETCH_SNAPSHOT" # we missed something; get the full bitmap
return "APPLY"On-sale and seat map
Q1/3 Why is a show's seat map served as a CDN layout plus a small bitmap of states?
Answer & explanation
Answer: B
Rows, gaps and categories are fixed for a show. Separating them leaves a few hundred bytes of state that changes, which is cheap to push and cheap to refetch.
Q2/3 A viewer's map is at version 418 and the next delta says "from 420 to 423". What should the app do?
Answer & explanation
Answer: C
Deltas only make sense in order. A gap means the local map may be wrong, so the app refetches the bitmap with its version and continues from there.
Q3/3 Why coalesce seat changes to at most one delta per show every 500 ms?
Answer & explanation
Answer: C
With 3,000 viewers and many changes a second, one message per change would multiply quickly. A short window keeps the map within about a second and bounds the fan-out.
Phase 5: Scaling and Edge Cases (5 minutes)#
"Walk me through a blockbuster's Friday, and then tell me what breaks."
A Blockbuster's Advance Booking#
- Days before · Pre-warm
- Shows are loaded for the opening weekend; layouts pushed to the CDN; bitmaps created on their shards
- Hottest shows (big multiplexes, evening slots) are spread across shards so no two of them share one
- Booking opens · The first hour
- Seat map opens jump about 5× (~3,000 a second); holds about 260 a second, concentrated on a few hundred shows
- Deltas coalesced per show; fan-out tier scaled for the viewers, the seat store barely notices
- Partner-owned shows
- Each chain's adapter runs at its own agreed rate; if a chain slows, its breaker opens and its shows say "try again shortly"
- Opening night
- Most bookings are last-minute: short holds matter most here, so abandoned seats return before the show
- Entry scanners mark tickets used; a second scan of the same QR is refused
Edge Cases#
Edge case 1: Payment succeeds after the hold expired
Problem: Ananya's UPI app took nine minutes. Her 8-minute hold expired and H7 was held by someone else; then the money arrived.
Solution:
sell()checks the hold id on every seat and fails cleanly: no seat is sold twice- The saga tries to hold the same seats again; if they are gone, it refunds automatically and says so: "Your payment will be refunded; those seats were released after 8 minutes"
- Payments still pending at the hold expiry keep the hold for a short grace period, so this stays rare
Edge case 2: Double tap on "Pay"
Problem: The button is tapped twice because the spinner was slow.
Solution:
- One idempotency key per checkout attempt, used for the hold, the booking and the payment
- The second tap returns the first's result: one charge, one booking
Edge case 3: The box office sold the seat
Problem: A chain-owned seat our cache showed as free was sold at the counter a minute ago.
Solution:
- The partner's hold call says "taken"; the customer is told which seat and picks again
- The adapter refreshes that show's cached map immediately
Edge case 4: Confirm times out after payment
Problem: Money captured, the chain's confirm call doesn't answer.
Solution:
- Booking goes to
CONFIRM_PENDING; the adapter asks for the hold's status, with backoff, until it is definite - Confirmed → ticket; gone → refund with a clear message. Never a ticket without a confirmed seat
Edge case 5: Resale and touts
Problem: Tickets bought by account farms appear on resale sites minutes after the sale.
Solution:
- Per-account limits at hold time; verified phone numbers to join; device and velocity checks
- For the biggest events, names on tickets and ID checks at entry, so a resold ticket is worth less
- Tickets are QR codes refreshed in the app close to the event, so screenshots shared early don't scan
Edge case 6: The orphan seat
Problem: Holding H5-H7 in a row of H5-H8 leaves H8 as a single seat nobody wants.
Solution:
- An optional rule in the hold check: reject a selection that leaves a single free seat between taken seats or at a row end, with a hint ("move one seat left")
- Never applied to the last few seats of a show, where any sale beats an empty seat
Scaling and edge cases
Q1/3 A payment confirms nine minutes after an 8-minute hold expired, and one of the seats now has someone else's hold. What happens?
Answer & explanation
Answer: C
The hold id is the capability to sell. Since another hold owns a seat, the sale can't happen; the money must end in a refund, automatically and with a clear message.
Q2/3 Why spread the hottest shows of a blockbuster across different shards before booking opens?
Answer & explanation
Answer: B
Each show has one writer, which is what makes holds atomic. Placing the hottest shows on different shards keeps one machine from carrying several of them at the peak.
Q3/3 A selection would leave a single empty seat at the end of a row. When should the platform refuse it?
Answer & explanation
Answer: C
Orphan seats rarely sell, so steering customers away from them sells more of the show. Near sell-out, any sale is better than an empty seat, so the rule switches off.
Phase 6: Monitoring and Operations#
"Two dashboards: the steady one for cinema, and the on-sale war room. The numbers that matter are contention, partner health and the funnel, not just CPU."
Monitoring Dashboard#
The funnel (this sale)
| Stage | Count |
|---|---|
| In the waiting room | 11,840,322 |
| Admitted to checkout so far | 29,415 |
| In checkout now (target 3,000) | 2,961 |
| Tickets sold / left / in holds | 61,208 / 81,377 / 7,415 |
Booking core
- Hold p99: 38 ms; holds failed "seat taken": 14%
- Checkouts completed: 21 a second; holds expired unpaid: 9%
- Payment success: 96.8%
Edge
- Status requests: 190,000 a second, 99.7% from cache
- Bot challenges served: 412,000; joins blocked: 96,000
Cinema (must be untouched)
- Hold p99: 41 ms (normal: 40 ms)
- Seat map p99: 210 ms
Alerts
- 12:00:00 (green) — Lottery drawn for 10.9 million pre-open arrivals
- 12:03:12 (amber) — Payment success below 97% on one card network, routed to the healthy gateway
- 12:06:40 (amber) — Chain C adapter timeouts at 4%, breaker half-open; cinema bookings for chain C paused 40 s
SLOs for the Platform#
SLO 1: Zero double-sold seats
- Target: 100%. Every seat has at most one booking, checked nightly against the durable log and partners' confirmations
- Error budget: 0
SLO 2: Money integrity
- Target: 100% of captured payments end in a ticket or a completed refund within 30 minutes
SLO 3: Hold latency
- Target: p99 < 500 ms on our inventory; partner holds answered or resolved within 10 s
SLO 4: Isolation
- Target: during an on-sale, cinema hold and seat-map latency stay within 10% of normal
SLO 5: Honest queue
- Target: "sold out" reaches everyone still waiting within 10 minutes of the last ticket being sold or held
Reading the war room
Q1/3 14% of holds fail with "seat taken". Is that a problem?
Answer & explanation
Answer: A
In a rush, people tap the same seats. A rising rate together with stale-map signals (version gaps, slow deltas) would point at the fan-out; contention alone is expected.
Q2/3 Why is cinema hold latency on the on-sale dashboard?
Answer & explanation
Answer: B
The promise is that an on-sale never slows daily booking. Watching cinema latency during the sale proves it, or catches a shared dependency nobody noticed.
Q3/3 9% of on-sale holds expire unpaid. What does the waiting room do with those tickets?
Answer & explanation
Answer: C
Expired holds put seats back and free checkout slots. That's why the room waits for holds before saying "sold out".
Final Challenge: A Stadium On-Sale on Blockbuster Friday#
The interviewer smiles. "Now both at once."
"Friday, 12:00 PM. A stadium concert goes on sale: 15 million people, 180,000 tickets across standing zones and seated blocks. At 12:00 PM the same day, advance booking opens for the year's biggest film across a few thousand screens, a third of them owned by cinema chains. Payments run through the same gateways. What do you do?"
- On-sale: 15 million waiting, 180,000 tickets (60,000 standing, 120,000 seated), limit 4
- Film: ~5× a normal busiest hour of cinema holds, a third via partner adapters
- Shared: payment gateways, the API gateway, the identity service
- Nothing may be sold twice, and the film's buyers must not notice the concert
Final challenge · Quest check
Q1/4 How should a 60,000-person standing zone be sold during the on-sale?
Answer & explanation
Answer: B
Standing tickets are identical, so a seat map adds nothing but contention. A counter with holds and expiry sells them exactly once, like Tatkal's pool.
Q2/4 Both the on-sale and the film share the payment gateways. What protects film buyers?
Answer & explanation
Answer: C
The on-sale's checkout rate is set by admissions, so it can be capped well under the gateways' capacity. Separate pools mean a slow concert payment can't hold a connection a film payment needs.
Q3/4 Why does "best available" help seated blocks during an on-sale?
Answer & explanation
Answer: B
Free choice in a rush concentrates everyone on the "best" seats and most holds fail. Server-side picking spreads buyers across free blocks, so far fewer holds lose races.
Q4/4 15 million people waiting with polling every 10 s is how many requests a second?
Answer & explanation
Answer: C
15,000,000 ÷ 10 = 1.5 million a second, just to say "not yet". That's why positions are signed tokens checked at the edge and polling slows with distance from the front.
Interview Conclusion#
"Good. A few quick ones to finish."
"Why not lock seats in the database with SELECT FOR UPDATE, one row per seat?"
"You can, if every hold for a show goes to one place and locks its seats in a fixed order, in one short transaction with a row-count check. What I'd avoid is long-lived locks held while the customer pays. The hold is a state with an expiry, not a lock: a crashed app or a walked-away customer then costs nothing."
"What's the single most important decision?"
"Being clear about who owns each seat. For our inventory, one writer per show with all-or-nothing holds. For a chain's inventory, their system decides and we treat every timeout as 'unknown' and ask. Everything else, the seat map, the queue, the fan-out, is there to make that one decision cheap and honest."
"And the on-sale?"
"Ninety-nine people in a hundred won't get a ticket. Make their wait cost almost nothing, make the order fair to humans, admit only as fast as tickets can sell, and say 'sold out' once, early, to everyone."
Summary: Concepts Applied from 10-Week Course#
- Week 1: Data at scale
- Partitioning — seat state sharded by show, hot shows spread across shards
- Week 2: Failure-first design
- Idempotency — one key per checkout across hold, booking and payment; partner calls keyed on our hold id
- Timeouts — partner timeouts treated as "unknown", resolved by status calls
- Bulkheads and circuit breakers — one compartment per cinema chain
- Week 3: Messaging and async processing
- Backpressure — admit to checkout on completions, not on a timer
- Change log and fan-out — seat changes to viewers as versioned deltas
- Week 4: Caching
- Thundering herd — the waiting room, edge-served status, polling that slows with distance
- Layout on the CDN, state in a tiny bitmap
- Week 5: Consistency and coordination
- Atomic multi-seat holds with expiry on one writer per show
- Saga — hold, pay, confirm with the owner, or release and refund
- Week 9: Security
- Per-account limits, lottery at the open, bot checks and step-up challenges
- Week 10: Production readiness
- SLOs for zero double-sale, money integrity and isolation; the on-sale war room
Why BookMyShow Matters#
Every unit is different
- Tatkal sells identical berths from a pool; a cinema sells H7, which is not H8. People choose, and their choices collide
- A booking is several seats that only make sense together
Inventory you don't own
- Big chains sell the same seats at the box office and on their own apps, so the platform must hold and confirm through someone else's system and live with "unknown"
The crowd is the load
- 1.3 crore people for ~1.5 lakh tickets: the cost is the waiting, not the selling
- Fairness is a design requirement, because the alternative is a race won by scripts
"Nobody remembers the seat map that was a second behind. Everybody remembers the seat that was sold twice."
Self-Assessment Checklist#
After studying this case study, you should be able to:
Architecture:
- Split a ticketing platform into a stale-tolerant read plane and an exact write plane
- Put owned and partner-owned inventory behind one hold interface
Seat holds:
- Write an all-or-nothing multi-seat hold with an expiry on one writer per show
- Explain why the hold id, not the customer id, authorises the sale
Partners:
- Treat a partner timeout as "unknown" and resolve it with idempotent status calls
- Isolate partners with bulkheads and circuit breakers
On-sales:
- Estimate the cost of waiting and design an edge-served waiting room
- Order a queue fairly with a lottery at the open, and admit on completions
- Decide when to stop admitting and say "sold out"
Operations:
- Push seat-map changes as versioned, coalesced deltas
- Define SLOs for zero double-sale, money integrity and isolation
Sources#
Company figures and reports:
- Book
AWS Big Data Blog — How BookMyShow saved 80% in costs by migrating to an AWS modern data architecture (up to 200 million tickets a year at its pre-COVID run rate; booking data in MariaDB and seat allocation data in MongoDB among the analytics sources), January 2023: https://aws.amazon.com/blogs/big-data/how-bookmyshow-saved-80-in-costs-by-migrating-to-an-aws-modern-data-architecture/
- Read
Business Today — From Pushpa 2 to Coldplay: BookMyShow 2024 report (2.3 million tickets in 24 hours on 1 November 2024; 30,687 live events across 319 cities), December 2024: https://www.businesstoday.in/technology/news/story/from-pushpa-2-to-coldplay-a-blockbuster-year-of-cinematic-and-live-experiences-bookmyshow-2024-report-458334-2024-12-24
- Read
Business Today — India turned entertainment into a lifestyle choice in 2025: BookMyShow report (6.8 million tickets over the Dussehra weekend; 34,000 events), December 2025: https://www.businesstoday.in/lifestyle/news/story/india-turned-entertainment-into-a-lifestyle-choice-in-2025-bookmyshow-report-506151-2025-12-10
The Coldplay on-sale (September 2024):
- Read
Outlook Business — Coldplay's concert demand reveals India's growing market for going-out experiences (1.3 crore logged in, ~1.5 lakh tickets, sold out in under an hour, sale from 12:15 PM, queueing system and "suspicious and malicious traffic"): https://www.outlookbusiness.com/explainers/coldplays-concert-demand-reveals-indias-growing-market-for-going-out-experiences
- Read
Onmanorama — Ensured every fan had fair chance: BookMyShow on Coldplay concert ticketing issue (13 million fans, 4 tickets per user, police complaint over unauthorised resale), September 2024: https://www.onmanorama.com/entertainment/entertainment-news/2024/09/29/bookmyshow-clarifies-coldplay-concert-tickets-india.html
- Read
Business Today — BookMyShow files police complaint amid Coldplay Mumbai concert ticket resales, September 2024: https://www.businesstoday.in/trending/entertainment/story/beware-of-such-scams-bookmyshow-files-police-complaint-amid-coldplay-mumbai-concert-ticket-resales-447390-2024-09-25
Comparable on-sales:
- Read
Ticketmaster — Taylor Swift | The Eras Tour onsale explained (3.5 billion total system requests, 4x the previous peak; 2 million+ tickets sold on 15 November 2022; Verified Fan registrations), November 2022: https://business.ticketmaster.com/business-solutions/taylor-swift-the-eras-tour-onsale-explained/
Taxes:
- Read
Business Today — GST revision: moviegoers get relief (5% on cinema tickets up to ₹100, 18% above, from 22 September 2025), September 2025: https://www.businesstoday.in/personal-finance/news/story/gst-revision-moviegoers-get-relief-in-small-screens-ipl-goers-face-40-rate-for-match-tickets-492552-2025-09-04
Design note: the architecture, code, hold times, thresholds, rates and dashboards in this case study are an illustrative design for the problem, not a description of BookMyShow's internal systems, which are not publicly documented beyond the sources above.
Further Reading#
Related systems to study:
- Read
IRCTC Tatkal (Bonus Problem 11 in this series): identical seats from a pool, an atomic counter and a waiting room; compare it with per-seat holds here
- Read
Razorpay Payment Gateway (Bonus Problem 18): what happens on the other side of "Pay", including unknown outcomes and refunds
- Read
Blinkit (Bonus Problem 12): reservations with expiry against physical stock
Foundational concepts (this course):
- Read
Week 2, Day 2 — Idempotency in Practice
- Read
Week 2, Day 1 — Timeout Hell
- Read
Week 2, Day 3 — Circuit Breakers
- Read
Week 3, Day 3 — Backpressure and Flow Control
- Read
Week 4, Day 3 — Thundering Herd
- Read
Week 5, Day 2 — Distributed Transactions: The Saga Pattern
- Read
Week 5, Day 1 — Consistency Models
"Thirteen million people, a hundred and fifty thousand tickets, and four seats in row H. The system that gets this right isn't the one that sells fastest. It's the one that knows who owns each seat, and tells everyone else the truth."
- 0/7stages cleared
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.