~learn.

BookMyShow Seat Booking, the whole design.

A ticketing platform where people pick exact seats from a live map, for cinemas every day and for on-sales of 13 million people, and no seat is ever sold twice because one writer per show holds a booking's seats all together or not at all.

7 diagrams · 8 sections · revise in ~10 min · Download PDF prints it in the light theme, diagrams included

SQ-16 · BookMyShow Seat Booking · design sheethimanshukukreja.in/learn/sheets/bonus-16-bookmyshow-seat-booking

At a glance#

Number Value What it drives
On-sale crowd · tickets 13 million · ~1.5 lakh ~1 buyer in 260; a waiting room, not a bigger database
Ticket limit 4 per person checked at hold time, across tabs and retries
Completed checkouts in an on-sale ~19 / s admit on completions, ~3,300 in checkout at once
Naive queue polling 1.3 million req / s status from the edge, polling slows with position
Cinema peak day (design target) 3 million tickets · 1.2 million bookings ~40 bookings/s, ~200/s when a blockbuster opens
Seat map opens ~600 / s · ~3,000 / s at a blockbuster layout on the CDN, state pushed as deltas
Every open seat map ~22 MB at 2 bits a seat space is not the problem; ownership is
Hold time 8 min cinema · 5 min on-sale expiry stored on the seat

Architecture#

DiagramSeat booking on one page
%% title: Seat booking on one page
flowchart TD
  U@{ icon: "sd:users", label: "Customers<br/>app + web" } --> CDN@{ icon: "sd:cdn", label: "CDN<br/>seat layouts, posters" }
  U ==>|"on-sales"| WR["Waiting room<br/>lottery at the open · signed tokens"]:::queue
  U ==> GW@{ icon: "sd:gateway", label: "API gateway<br/>auth · rate limits · bot checks" }
  WR ==>|"admitted only"| GW
  GW --> MAP@{ icon: "sd:cache", label: "Seat map cache<br/>2-bit bitmap per show" }
  GW ==> HOLD["Hold service<br/>all or nothing · 8-min expiry<br/>one writer per show"]:::hot
  GW ==> BK["Booking saga<br/>hold → pay → confirm → ticket"]:::core
  HOLD ==> SEAT@{ icon: "sd:database", label: "Seat store<br/>our inventory, sharded by show" }
  HOLD ==> ADP["Chain adapters<br/>hold id as ref · breaker per chain"]:::service
  ADP --> POS@{ icon: "sd:server", label: "Chain ticketing systems<br/>box office, own apps" }
  BK --> PAY@{ icon: "sd:card", label: "Payment gateways" }
  BK -.->|"sell by hold id"| HOLD
  SEAT -.-> LOG@{ icon: "sd:stream", label: "Seat change stream<br/>versioned deltas" }
  LOG --> MAP
  class U client
  class CDN,GW edge
  class MAP cache
  class SEAT store
  class LOG queue
  class POS,PAY external
DiagramThe on-sale front door
%% title: The on-sale front door
flowchart TD
  U@{ icon: "sd:users", label: "13 million people" } ==> EDGE@{ icon: "sd:cdn", label: "Edge<br/>static page · cached status" }
  EDGE ==> BOT@{ icon: "sd:shield", label: "Bot checks<br/>device · account age · velocity" }
  BOT ==> LOT["12:00:00 lottery for everyone waiting<br/>then arrival order"]:::queue
  LOT ==> ADM{"Room in checkout<br/>and tickets left after holds?"}
  ADM ==>|"yes"| CK["Checkout<br/>~3,000 at once · 5-min holds"]:::hot
  ADM -->|"tickets gone"| SO(["Sold out · said once, to everyone"]):::bad
  CK -.->|"finished or expired · free a slot"| ADM
  class U client
  class EDGE,BOT edge

Key flows#

DiagramFour seats in row H, on our inventory
%% title: Four seats in row H, on our inventory
sequenceDiagram
  autonumber
  participant A as 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
  R-->>A: Layout from CDN + bitmap v418
  A->>H: Hold H5-H8 (key k1)
  H-->>A: Held, all 4 · expires in 8 min (hold h1)
  A->>B: Checkout h1 (key k1)
  B->>P: Create payment (key k1)
  P-->>B: Success
  B->>H: Sell h1
  H-->>B: Sold · v419
  B-->>A: Ticket (QR)
  Note over R: v419 pushed to every viewer · H5-H8 turn grey
DiagramA chain's system doesn't answer
%% title: A chain's system doesn't answer
sequenceDiagram
  autonumber
  participant H as Hold service
  participant D as Chain adapter
  participant C as Chain system
  H->>D: Hold seats (ref h1, ttl 8 min)
  D->>C: POST hold · ref h1
  C--xD: no reply in 3 s
  Note over D: unknown · never send a second hold with a new ref
  D->>C: Status of h1
  alt hold exists
    C-->>D: Held until 19:03
    D-->>H: Held · show the customer an earlier expiry
  else no such hold
    C-->>D: Not found
    D-->>H: Not held · customer picks again
  end
DiagramPaid after the hold expired
%% title: Paid after the hold expired
sequenceDiagram
  autonumber
  participant B as Booking saga
  participant H as Hold service
  participant P as Payment
  P-->>B: Success after 9 min
  B->>H: Sell h1
  H-->>B: Lost · H7 now has another hold
  B->>H: Hold the same seats again
  alt seats still free
    H-->>B: Held · then sold
  else taken
    H-->>B: Taken
    B->>P: Refund (keyed on the booking)
  end

Data model#

DiagramCore entities
%% title: Core entities
classDiagram
  class Show {
    +showId : string · shard key
    +owner : OURS or CHAIN
    +layoutId : string · on the CDN
    +version : int · bumped on every change
  }
  class Seat {
    +seatId : string · H7
    +state : FREE / HELD / SOLD
    +holdId : string
    +expiresAt : epoch ms
    +bookingId : string
  }
  class Hold {
    +holdId : uuid · partner ref too
    +idemKey : string
    +seats : list of seat ids
    +expiresAt : epoch ms
  }
  class Booking {
    +bookingId : string
    +state : BookingState
    +amount : paise
    +partnerRef : string
  }
  class QueueToken {
    +saleId : string
    +accountId : string
    +position : float
    +signature : hmac
  }
  Show "1" --> "many" Seat : has
  Hold "1" --> "many" Seat : holds
  Booking "1" --> "1" Hold : sells
  QueueToken "many" --> "1" Show : waits for

Lifecycle#

DiagramLife of one seat
%% 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 expired
  SOLD --> USED: scanned at the venue
  SOLD --> FREE: cancelled where allowed
  USED --> [*]
  class SOLD,USED ok
  class HELD queue

Decisions#

Decision Chose Over Because
Multi-seat holds One atomic check-and-set per show Seat-by-seat locks no deadlocks, no partial sets
Abandoned holds Expiry stored on the seat Sweeper or client release correct even if the sweeper runs late
Selling By hold id By customer id a late payment can't sell re-held seats
Chain-owned seats Chain's system decides, via adapter Our cached copy box office and own apps sell the same seats
Partner timeout Unknown, then status call Retry or assume failure no double holds, no stuck seats
On-sale order Lottery at the open, then arrival order First come, first served racing the clock is worthless to scripts
Admission On completed or expired checkouts Fixed rate follows what checkout really absorbs

When it breaks#

Failure What users see Contain it with
Two families tap overlapping seats one gets all 4, the other picks again atomic all-or-nothing hold
Payment after the hold expired seats if still free, else auto refund sell by hold id, re-hold, refund
A chain's system is down that chain's shows paused bulkhead + breaker per chain
13 million wait at noon a queue page, not an error edge-served status, slower polling far back
Tickets resold by account farms fewer touts, ID checks for the biggest events per-account limits, bot checks, refreshed QR codes

Say it in the interview#