Handbooks / System Design Case Studies / Chapter 14
Design a Ticket Booking System like BookMyShow
12 pages · ~19 min✓ Reviewed
Builds on Design a Ride-Hailing App like Uber.
Problem Statement, Scope and Requirements (BookMyShow)
1. Scope and Functional Requirements
| In scope | Out of scope |
|---|---|
| Browse cities, theatres, shows | Ads |
| Pick seats | Reviews |
| Pay | Food ordering |
| Get e-ticket | Theatre onboarding UI |
| Cancel with refund | - |
- Search shows by city, movie and date
- View the live seat map of a show
- Hold chosen seats for 8 minutes
- Pay, then confirm the booking
- Cancel, then refund per policy
2. Scale Estimates
- 5M bookings/day50M MAU
- ~60/s average5M / 86,400 s
- 600/s peak10x in evening hours
- ~3,300 req/sone blockbuster
High-Level Architecture (BookMyShow)
1. The Seven Services
| Service | Owns | Store |
|---|---|---|
| Catalog | cities, theatres, movies, shows, seat layout | Postgres + CDN |
| Show and Seat Inventory | seat status, holds, seat-level bookings | sharded Postgres + Redis |
| Booking | booking record and its states | Postgres |
Catalog, Shows and Seat Maps (BookMyShow)
1. Responsibilities
- Serve cities, theatres, movies and shows (browse is cached and CDN-fronted)
- Render a seat map with per-seat status
- Seat status is one of AVAILABLE, HELD, BOOKED
- Read target: seat map p99 under 200 ms, up to ~3,300 req/s on one blockbuster
2. Design and Flow
- Static layout: rows, columns, categories; rarely changes, cached at the CDN
Seat Hold with Expiry (BookMyShow)
1. Responsibilities and Design
- Reserve up to {6 seats} for 8 minutes while the user pays
- Then auto-release the seats if no payment arrives
- DB row: hold with status
HELD,hold_id,expires_at; the source of truth - Redis key:
hold:{show}:{seat}with TTL 480 s, only a fast release signal
- Hold is a status, never a DB lock held for 8 minutes
- Max 6 seats per hold, all-or-nothing
Preventing Double Booking (BookMyShow)
1. The Invariant and the Last Guard
- One (show_id, seat_id) can be BOOKED by at most one booking
- Enforce it in the database, never only in app code
- App checks can be bypassed by a bug, a retry or a second service
- Last safety net: unique constraint on
booking_seat(show_id, seat_id) - A buggy path then fails with a duplicate-key error instead of double booking
- On cancel, delete the booking_seat rows so the seat can be sold again
Payments and Booking Confirmation (BookMyShow)
1. Responsibilities and Design
- Charge the user for a hold (hold_id, 8 min window)
- Confirm the booking exactly once
- Issue the ticket with a QR code
- Payment service wraps UPI, card, wallet behind one interface
- The gateway webhook confirms success, not the browser redirect
2. Confirmation Flow
- Intentpayment for hold_id
- Redirectuser pays at gateway
- WebhookHMAC verified
- One DB txnseats BOOKED, booking CONFIRMED
- Eventemit BookingConfirmed
Virtual Queue for High-Demand Releases (BookMyShow)
1. Role and Design
- Shield Inventory and Payment from a flash crowd of 1M users
- Admit fairly: first come, first served, in controlled batches
- Queue sits in front of the booking flow, not inside it
- Enabled per movie by ops, or auto-on when requests pass 2,000/s
- Waiting page and status reads: served from the CDN edge
- Queue order: Redis sorted set
- Normal movies skip the queue entirely
Cancellations and Refunds (BookMyShow)
1. Responsibilities and Policy
- Let users cancel before a cutoff
- Release the seats back to inventory
- Return money per policy, to the original payment method
- Theatre-cancelled show: refund 100% automatically, no user action
| When | Refund |
|---|---|
| Until 4 h before showtime | 100% of ticket price |
Notifications and Ticket Delivery (BookMyShow)
1. Responsibilities and Design
- Send booking confirmation and the QR ticket after payment
- Send show reminders and cancellation / refund updates
- Channels: push, SMS, email
- Kafka topicsBookingConfirmed, BookingCancelled, RefundProcessed
- Consumersone group per topic
- Channel workerspush, SMS, email
- Providerprimary, then fallback
Scaling, Failure Handling, Trade-offs and Interview Questions (BookMyShow)
1. Scale and the Hot Show
- Shard the seat store by show_id: one show is one shard, so no cross-shard transactions
- Read replicas and Redis serve seat maps; writes go only to the shard primary
- 5M bookings/day = ~60/s avg, 600/s peak: fits a few Postgres shards
- Per-show seat rows: 10M seat rows/day across all shows
- Routerhash(show_id)
- Shard primaryholds, bookings
- Replicas + Redisseat-map reads
Cheatsheet: Movie Ticket Booking Design (BookMyShow)
1. States and Numbers
- AVAILABLEfree
- HELD8 min TTL
- BOOKEDpaid
- CONFIRMED
- CANCELLING
- CANCELLED
- REFUNDED
| Number | Value |
|---|---|
| Bookings, average | 60/s |
| Bookings, evening peak (10x) | 600/s |
Check yourself
Spot the bug: two users click seat 12 of show 7 at the same instant. Both calls run the code below. What can happen, and what is the fix?
- Both SELECTs can return
AVAILABLEbefore either UPDATE runs, so both users get a hold: a check-then-update race. - The UPDATE has no condition on status, so the second one silently overwrites the first.
- Fix: one conditional statement,
UPDATE ... SET status='HELD' WHERE show_id=7 AND seat_id=12 AND status='AVAILABLE'. - Rows affected = 1 means you won the seat. Rows affected = 0 means you lost the race, so reject that user.
- The unique (show_id, seat_id) on
booking_seatstays as a last safety net, not as the main guard.
SELECT status FROM show_seat WHERE show_id=7 AND seat_id=12; -- app code: if status == 'AVAILABLE' ... UPDATE show_seat SET status='HELD' WHERE show_id=7 AND seat_id=12;