Design a Ticketing System (BookMyShow / Ticketmaster)
Difficulty: Tier 2 Asked at: Amazon, BookMyShow, Ticketmaster, Careem Time budget: 45 min
Booking a concert or movie seat sharpens the overselling problem from e-commerce to
a knife’s edge: seats are unique (seat 14C, not “one of 100 units”), demand is spiky (tickets go
on sale at 10am and sell out in seconds), and a seat must be held while the user pays but released
if they don’t. The heart is seat reservation with expiry under extreme concurrency.
Prerequisites: Distributed Locking, Transactions, Design E-Commerce
1. Requirements
Functional:
- Browse events/shows; view a seat map with availability.
- Select and hold specific seats while the user completes payment.
- Book (confirm) held seats; release them if payment isn’t completed in time.
- Prevent two users from booking the same seat.
Non-functional:
- No double-booking — a seat goes to exactly one person (strong consistency on seat state).
- Handle spikes — a hot event: thousands/sec contending for the same show’s seats at on-sale.
- Fair-ish and responsive; users get quick feedback (“seat taken”).
- Read-heavy browsing vs contended booking.
Out of scope: payment internals (separate), recommendations, dynamic pricing.
2. Estimation
- A popular show: 50,000 seats, 500,000 people hitting “buy” in the first minute → ~10,000
booking attempts/sec all contending for the same show’s seats. 🚨 Extreme contention on a small,
fixed set of unique resources — the defining challenge.
- Browsing (seat maps, event pages) is read-heavy and cacheable; booking is the hard, contended write path.
3. The core: seat reservation with expiry
🚨 A seat has three states: AVAILABLE → HELD (reserved, with a TTL) → BOOKED. The tricky part is the
HELD state — a temporary, expiring lock so a user can pay without losing the seat, but the seat frees up
if they abandon.
stateDiagram-v2
AVAILABLE --> HELD: user selects (TTL, e.g. 5 min)
HELD --> BOOKED: payment confirmed
HELD --> AVAILABLE: TTL expires / user cancels
BOOKED --> AVAILABLE: refund/cancel (policy)
Holding a seat = an atomic conditional update: set seat HELD by user X WHERE seat is AVAILABLE. If it
fails, someone else got it → tell the user immediately. The hold carries an expiry; a background sweep (or a
TTL in Redis) releases expired holds. 🚨 This “reserve-then-confirm with a TTL” is the whole trick —
it’s the same pattern as inventory reservation in e-commerce, but per unique seat.
4. High-level design
flowchart TB
Client -->|view seat map| Read[Read path: cached availability]
Client -->|hold seat| Booking[Booking Service]
Booking -->|atomic hold + TTL| SeatDB[(Seat state store<br/>strongly consistent)]
Booking --> Hold[(Hold registry<br/>Redis, TTL)]
Client -->|pay| Payment[Payment Service]
Payment -->|success| Booking
Booking -->|HELD -> BOOKED| SeatDB
Sweeper[Expiry sweeper] -.releases.-> SeatDB
Seat state lives in a strongly-consistent store (relational, or Redis with atomic ops for holds + a
durable DB of record for bookings). The booking service serializes seat-state transitions.
5. Deep dives
5a. Atomic hold — preventing double-booking
Two users clicking seat 14C at once: the atomic conditional update ensures only one succeeds (the DB
serializes it; the loser sees “seat unavailable”). Use a row lock / SELECT ... FOR UPDATE in a transaction,
or an atomic Redis operation, or a per-seat distributed lock.
🚨 Never read-then-write without atomicity — that’s the classic race that oversells.
5b. The hold TTL and its expiry
A hold must auto-release if the user doesn’t pay. Options:
- Redis key with a TTL — the hold vanishes automatically; simplest.
- A
held_until timestamp in the DB + a sweeper job that releases expired holds, and a check at
booking time (never book a seat whose hold expired). 🚨 Combine lazy (check-on-use) + active (sweeper) so
correctness doesn’t depend solely on the sweeper running.
- Handle the race: user pays just as the hold expires — decide a policy (grace period, or fail and refund).
5c. Handling the on-sale spike (thundering herd)
10,000/sec for one show is a hotspot. Mitigations:
- Virtual waiting room / queue — admit users in controlled batches so the booking system isn’t
overwhelmed (Ticketmaster does this). Users get a queue position.
- Cache the seat map for browsing, but booking always hits the consistent store.
- Fast rejection: tell users a seat’s gone immediately rather than making them wait. (Thundering Herd, Hot Keys)
5d. Consistency vs the read path
Seat maps shown to browsers can be slightly stale (cached) — a user might click a seat that was just
taken; the atomic hold catches it. 🚨 Eventual consistency for display, strong consistency for the actual
hold/booking — don’t make every seat-map view hit the consistent store, but never trust the cached view
for the booking decision.
5e. Booking confirmation & payment
Hold → user pays → on payment success, transition HELD→BOOKED atomically and durably record the booking.
If payment fails or times out, release the hold. Use idempotency so a retried payment/confirmation doesn’t
double-book or double-charge. Multi-seat orders: hold all seats, book all-or-nothing (transaction).
6. Bottlenecks & scaling further
- Seat contention → atomic conditional holds; per-show, per-seat granularity.
- On-sale spike → virtual waiting room, batched admission, fast rejection.
- Hold expiry correctness → TTL + sweeper + check-on-book (belt and suspenders).
- Read load (seat maps) → cache; tolerate slight staleness.
- Multi-show scale → partition by event/show; each show’s contention is isolated.
7. Trade-off summary
| Decision |
Chosen |
Alternative |
Why |
| Seat hold |
Atomic conditional update + TTL |
Read-then-write |
Prevents double-booking under concurrency |
| Expiry |
TTL + sweeper + check-on-book |
Sweeper only |
Correctness doesn’t hinge on one mechanism |
| On-sale spike |
Virtual waiting room |
Let all in |
Protects the booking core from overload |
| Seat map display |
Cached (eventual) |
Always consistent read |
Cheap browsing; hold is the real gate |
| Seat state store |
Strongly consistent |
Eventually consistent |
A seat must go to exactly one person |
8. Follow-up questions
How do you stop two people from booking the same seat?
By making the seat hold an atomic conditional operation that the datastore serializes, so concurrent
attempts are resolved to exactly one winner. When a user selects seat 14C, you attempt to transition it from
AVAILABLE to HELD *conditionally* — only if it's still AVAILABLE — using a row lock (`SELECT ... FOR UPDATE`
in a transaction), an atomic compare-and-set in Redis, or a per-seat distributed lock. The datastore
processes concurrent holds for the same seat one at a time, so the first succeeds and every other sees the
seat is no longer AVAILABLE and is immediately told "seat taken." The essential rule is never to read
availability and then separately write the hold without atomicity, because that read-then-write gap is
exactly where two users both see "available" and both proceed — the atomic conditional update closes that
gap.
Why hold a seat temporarily instead of only booking on payment?
Because payment takes time (entering card details, 3-D Secure, processor round-trips), and without a hold two
bad things happen: either the seat stays bookable during that window so someone else can grab it out from
under a paying user, or you'd have to book-then-refund on payment failure, which is messy and can double-sell
in the meantime. A temporary hold with a TTL reserves the specific seat for the user for a few minutes so
they can pay in peace, while guaranteeing the seat isn't lost to anyone else during that window. The TTL is
what keeps it fair and prevents abandoned selections from locking seats forever — if the user doesn't
complete payment in time, the hold expires and the seat returns to AVAILABLE for others. It's the reserve-
then-confirm pattern, adapted to unique seats: reserve on selection, confirm on payment, auto-release on
timeout.
A blockbuster show goes on sale and 10,000 requests/sec hit the same seats. How do you cope?
The core seat-state store can't sanely take 10,000 contended writes/sec on one show's seats, so you protect
it with a virtual waiting room that admits users in controlled batches rather than all at once — users get a
queue position and are let through to the booking flow at a rate the system can handle (this is what
Ticketmaster-style systems do). Browsing the seat map is served from cache so read load doesn't touch the
consistent store. And you fail fast: the moment an atomic hold loses, tell that user the seat's gone instead
of making them wait, freeing capacity. Partitioning by show means this hot event's contention is isolated to
its own seats and doesn't affect other events. The combination — queue to smooth admission, cache for reads,
fast rejection, and per-show isolation — turns an unmanageable instantaneous spike into a controlled flow the
strongly-consistent booking core can serve correctly.
What if a user's hold expires at the exact moment they complete payment?
This is a real race that you must resolve with an explicit policy rather than leaving to chance. The safe
default is that the booking confirmation re-checks the hold atomically: transition HELD→BOOKED only if the
hold is still valid (not expired and still owned by this user), in one atomic operation. If the hold already
expired and the seat was reclaimed, the booking fails and you refund/void the payment and inform the user.
To reduce how often this bites real users, many systems add a short grace period beyond the visible TTL, or
stop accepting new payment attempts slightly before expiry. The key points are: never book on a stale
assumption that the hold is still valid — verify atomically at confirmation time — and always pair a failed
booking with a payment reversal so the user is never charged for a seat they didn't get. Combining lazy
check-on-book with the active sweeper ensures neither mechanism alone has to be perfect.
9. What junior / mid / senior answers look like
- Junior: marks a seat booked on selection or reads-then-writes without atomicity — either locks seats
forever or double-books under concurrency.
- Mid: three-state seat model with atomic conditional holds and a TTL, strong consistency on seat state,
cached seat maps for browsing.
- Senior: all that plus belt-and-suspenders expiry (TTL + sweeper + check-on-book), a virtual waiting
room for on-sale spikes, the expiry-vs-payment race handled by policy, idempotent all-or-nothing multi-seat
booking, and the explicit eventual-for-display / strong-for-booking consistency split.
Further reading