Skip to content
Back to the libraryProblem

Design: ticket booking

Sell every seat exactly once while lakhs of fans arrive in the same second. An atomic hold with an expiry, a virtual waiting room, keyed payments and a seat store that is the only truth.

The full lesson, with a worked example: Design: ticket booking. The fan's side is in Choosing a seat on BookMyShow and How Tatkal survives 10 AM.

Requirements

Functional

  • Browse: see shows and which seats are free.
  • Hold: hold one or more seats for a few minutes while paying.
  • Pay and get a ticket: by app, SMS and email.
  • Release: seats go back on sale if payment fails or time runs out.

Non-functional

  • Never sell a seat twice. Absolute. A slow page is annoying; two people at the gate with one seat is a disaster.
  • Stay up and fair in a rush: first come, first served, and the site doesn't collapse for everyone.
  • Fast for normal use: most of the day there is no queue at all.

Strict correctness for the seat; speed and fairness for everything around it.

Estimates

Say a stadium concert in Mumbai: 50,000 seats, 10 lakh fans at noon.

  • Data is tiny: 50,000 seats × about 100 bytes ≈ 5 MB. One show fits in one machine's memory.
  • Orders are few: at about 2.5 seats per order ≈ 20,000 orders.
  • The crowd is huge: about 20 requests per fan (map loads, taps, refreshes) ≈ 2 crore requests in the first minutes, over 1 lakh a second at peak.
  • Admit what can buy: 2,000 fans a minute × 2.5 seats ≈ 5,000 seats a minute, so a sell-out in about 10 minutes.
  • Inside at once: about 5 minutes each ≈ 10,000 fans, one request every 15 seconds or so ≈ under 1,000 requests a second. Holds: maybe 50 a second.
  • Waiting page: 10 lakh fans polling every 20 seconds ≈ 50,000 requests a second, all from a CDN.

So it's a and problem, not a data problem.

API

  • GET /v1/shows/:showId/seats returns the from cache, up to a second old.
  • POST /v1/queue/:showId/join returns a place and an estimate; GET /v1/queue/:showId/position is polled from the CDN. When admitted, the fan gets a signed pass valid for about 10 minutes.
  • POST /v1/holds with the pass, showId and seatIds. Returns holdId and heldUntil, or 409 with the seats that just went and nearby alternatives.
  • POST /v1/holds/:holdId/pay uses holdId as the payment's idempotency key. Returns PENDING or BOOKED.
  • DELETE /v1/holds/:holdId releases seats when the fan backs out.
  • GET /v1/bookings/:bookingId returns the ticket and its QR code.
  • Every booking call without a valid pass is rejected during a rush. Limits like 6 tickets per account apply at hold time.

Data model

  • shows: show_id, venue_id, starts_at, sale_opens_at, max_per_account.
  • seats: one row per seat per show: show_id, seat_id, section, price_paise, state (FREE, HELD, BOOKED), holder_id, held_until, version. Key on (show_id, seat_id).
  • holds: hold_id, show_id, account_id, seat_ids, held_until, status, payment_id.
  • bookings: booking_id, hold_id (unique), account_id, seat_ids, amount_paise, qr_code.
  • General admission: no seat rows, just a of tickets left per show and section.
  • Placement: by show_id. A mega-event gets its own shard, warmed before sales open.

High-level design

Step by step9 steps
  1. 1

    FanWaiting room

    Coldplay, Mumbai, at 12:00:00

  2. 2

    Waiting roomFan

    You are number 18,402. Signed pass when it is your turn

  3. 3

    FanBooking service

    Hold F7 and F8 (with pass)

  4. 4

    Booking serviceSeat store

    F7, F8 to HELD only if both FREE, until 12:08

  5. 5

    Seat storeBooking service

    Changed 2 rows: held

  6. 6

    Booking servicePayments

    Collect Rs 9,000 (key hold-881)

  7. 7

    PaymentsBooking service

    Paid (webhook)

  8. 8

    Booking serviceSeat store

    F7, F8 to BOOKED

  9. 9

    Booking serviceTicket queue

    Send ticket, SMS and email

RushSteady flowShow seatsHold, bookKeyed chargeFree expiredVia queueFansWaiting roomSeat map cacheBooking servicePaymentsSeat storeExpiry sweeperTickets and SMS
  • Waiting room: a hands out places and signed passes, and admits fans at a set pace.
  • Seat map cache: read constantly, so served from a and allowed to lag. The hold step is the real check.
  • Booking service: servers that check the pass, run holds and talk to payments.
  • Seat store: the , one row per seat, with synchronous replication.
  • Payments: a called with the hold ID as the .
  • Ticket queue: tickets, SMS and email go through a , so a slow SMS provider never blocks a booking.

Deep dives

Claiming a seat exactly once

  • Read-then-write is a : two fans both read FREE, both write HELD.
  • Make it one conditional write, a : "set F7 to HELD by Asha until 12:08, only if FREE (or its hold has expired)". The store returns how many rows changed: 1 means you got it, 0 means someone beat you.
  • Several seats, all or nothing: one ; if the changed count is less than the seats asked for, roll back. Sort seat IDs before updating so two multi-seat holds can't block each other.
  • No long locks: a row lasts the few milliseconds of the update. The hold is data in the row, never a lock kept open while the fan pays.
  • Version numbers () suit low contention. When everyone wants F7, retries explode; the conditional write is better.
  • General admission: decrement the counter only if above zero.

Holds that expire

Hold if FREEPaid in timeExpired, failedFreeHeld for 8 minBooked
  • Typical hold: 8 to 10 minutes. Too short (3 minutes) and slow UPI or OTP payers lose seats mid-payment. Too long (30 minutes) and abandoned carts make the show look sold out; touts love long holds.
  • Lazy expiry: every query treats a passed held_until as free, so a seat is available the instant its time runs out.
  • Sweeper: a background job every few seconds turns expired holds back to FREE and refreshes the seat map cache.
  • Pending payment: extend the hold by a few minutes, say 5, rather than release seats while money may be moving.

The waiting room

  • Each fan gets a place and a pass with a , so it can't be forged. The position page is mostly static and served from a .
  • Admission rate comes from what the booking service and seat store can handle, adjusted live: speed up when fans buy fewer seats or leave, stop once remaining seats are spoken for by people inside.
  • Fairness: fans waiting before opening get random places at 12:00, so refreshing faster wins nothing; later arrivals join the back in order. One place per account or phone, however many tabs.
  • Bots: before the line, verified phones, per account and address, per-account hold limits, and flags on hold-and-expire patterns.
  • The simplest waiting room is a rate limiter in front of booking, set to what the server can handle; see Pattern card: rate limiting.

Paying without losing money or seats

  • The hold ID is the payment key, so a retry after a timeout never charges twice.
  • Pending is a real state: wait for the or ask the gateway for the status.
  • Late money: if payment lands after the hold expired and someone else now holds the seats, match it by key and refund automatically, and tell the fan. If the seats are still free, re-hold and book them instead.
  • Nightly compares the gateway's payments with bookings; any payment with no booking is refunded.

The very hot show

  • One concert's seats are a : 95% of all holds land on one show's rows.
  • Shard by show and give announced mega-events their own shard and their own waiting room, with caches warmed in advance (). A Tuesday film in Indore books normally meanwhile.
  • takes minutes; the spike lasts seconds. Add servers beforehand, but admission control is the main defence.
  • A seat-store pauses admissions for a few seconds. Holds are safe to retry, because "hold F7 for Asha" is harmless if it already happened.

Trade-offs

  • Queue or open door: the waiting room adds a visible wait, but the people inside actually finish booking.
  • Fresh map or fast map: a cached map may show a seat that's gone. The cost is a "pick again" message, never a double sale.
  • Hold length: longer is kinder to slow payers and to touts. Most sites use 8 to 10 minutes, extended only for pending payments.
  • Database or in-memory store for holds: an in-memory store with an atomic set-if-absent is fast, but failover can lose granted holds. Use it as a first check at most; the durable seat store makes the final claim.
  • Strictness only where needed: only the seat store must be exact. Maps, notifications and analytics can lag, which keeps the system cheap.

Interview talking points

  • Say "never sell a seat twice" first, then show the data is tiny and the spike is huge.
  • The atomic conditional hold with an expiry; FREE, HELD, BOOKED.
  • The waiting room, with admission tied to capacity and seats left.
  • Payments keyed by hold ID, pending as a state, refunds for late money, nightly reconciliation.
  • Cached seat map with the seat store as the only truth.
  • Bots and per-account limits; a dedicated shard for the hottest events.

Follow-up questions

Why not a distributed lock per seat in Redis? It works as a fast first check, but a failover can lose locks it already granted. The final claim must still be a conditional write in the durable seat store.

How do you handle general admission? One counter of tickets left, decremented atomically only if above zero. For very hot events, split it across a few counters on different shards.

How do you pick the admission rate? Start from measured capacity of the booking service and seat store, then adjust live: faster when fans leave or buy fewer seats, stop once the seats left are spoken for by people already inside.

How is IRCTC Tatkal different from a concert? Same shape: a burst at one exact second, a few hundred berths, a strict rule against double sales. Passengers ask for a number of berths in a class rather than picking from a map, so the hold is closer to "take 2 from this class's quota if 2 are left" than to claiming named seats.

What does a fan see when the seat store fails over mid-sale? A short pause. Synchronous replication means no confirmed hold is lost, admissions pause for the few seconds of failover, and the app quietly retries holds, which are safe to repeat.