System design · Orders and money
Design a BookMyShow-style ticket booking system
- Reported in several places
- SDE-2
- 2 diagrams
- 2 sources
Two or more independent public pages, at least one a candidate write-up.
Design an online booking system for movie tickets with reserved seats. On an opening night, 200,000 people chase 300 seats within the first minute; no seat may be sold twice, and a user who picks seats should get a few minutes to pay before they are released.
A representative prompt in the style reported, not a quote from Flipkart. The answer below is written for this site.
Ask first
Clarify the problem before you draw anything.
Before drawing anything I would ask: are seats reserved (a specific seat) or general admission? Is our database the single source of truth for inventory, or do theatres run their own systems we must sync with? How long may a user hold seats while paying? What is the worst crowd: a normal evening, or one opening night where a huge crowd chases a few hundred seats? Is payment handled by an external provider? Do we need cancellations and refunds?
I would assume reserved seating, our own inventory as the source of truth, a 5-minute hold, an external payment provider, and the opening-night crowd as the design case. In scope: browse, seat map, hold, pay, confirm. Out of scope: theatre onboarding, recommendations, food orders and the seat-picker UI.
Numbers
A back-of-envelope that decides the shape.
These are planning figures for the exercise, not facts about any company.
- Inventory: 1,000 cinemas × 5 screens × 5 shows a day = 25,000 shows; × 200 seats = 5 million seat rows a day; × 100 B = 500 MB a day. A 30-day booking window is 15 GB, which fits on one relational primary.
- Normal browsing: 20 million seat-map views a day is about 20 × 12 = 240 a second; at a 10× peak, 2,400 a second. That is cache territory, nowhere near a database limit.
- Opening night: 200,000 users in the first minute, each making about 3 hold attempts: 600,000 ÷ 60 = 10,000 hold attempts a second, all aimed at the same 300 rows.
- At most 300 seats can be won, so at most 300 of 600,000 attempts succeed: 99.95% must fail, and they must fail cheaply.
The numbers say volume is easy and contention is the problem. 10,000 conflicting writes a second is twice the 5,000 committed writes a second planning figure for one primary, and they all queue on 300 row locks. So I admit users to the hold step at a controlled rate (500 a second drains 200,000 in 400 seconds) and answer "sold out" from a cache once the count reaches zero.
The design
Diagrams first, then components, data and the hard parts.
- The user opens the seat map, served from a cache that may be a few seconds stale.
- On a hot show the user joins a virtual queue instead of hitting the hold endpoint.
- Admitted users receive a short-lived token, and the hold endpoint rejects requests without one.
- The booking service runs one conditional update per seat, in sorted order, inside one transaction.
- The user pays; the call to the provider carries a key derived from the booking id.
- Confirming the booking writes an outbox row that ticket and notification workers consume.
Open on the whiteboardAll 2 diagrams on one tidy board, with a step-by-step walkthrough.
Components. A CDN and cache for show listings and seat maps; a booking service; one relational database (shows, seats, bookings); an external payment provider; a queue for tickets and notifications; and a small admission service in front of the hold step for hot shows.
Data model.
show_seats(show_id, seat_id, state, held_by, hold_expires_at, booking_id), primary key(show_id, seat_id). State is available, held or booked. One row per seat per show is the unit of contention, so every correctness rule is a rule about one row.bookings(booking_id, user_id, show_id, status, amount, idempotency_key)with a unique index on(user_id, idempotency_key).
Read path. The seat map is served from a cache keyed by show with a TTL of 2 to 5 seconds, rebuilt from the database on a miss and optionally refreshed when a hold or booking commits. It is allowed to be a little stale. It is a hint; the hold statement is the authority, and the UI must handle "that seat was just taken" gracefully.
Write path: hold.
- The user picks up to 6 seats (a per-user limit stops hoarding). The service sorts the seat ids so two overlapping requests update rows in the same order and cannot deadlock.
- One transaction runs a conditional update for each seat, in that sorted order: set state to held,
held_byto the user andhold_expires_atto now plus 5 minutes, where the seat is available or held with an expiry in the past. Use the database clock, not the app server's. - If any update changes no row, roll back. Holds are all or nothing. There is no read-then-write; for each seat the check and the claim are one statement, so two users cannot both pass the check.
Expired holds are released lazily by that predicate, and a sweeper also flips them back so the seat map stays accurate.
Write path: pay and confirm. Create a pending booking with an idempotency key, then call the payment provider with a key derived from the booking id so a retry cannot charge twice (the payments walkthrough covers the chain). When payment succeeds, one transaction marks the seats booked and the booking confirmed, and writes an outbox row that the ticket and notification workers consume. If the provider call times out, the outcome is unknown: keep the hold alive and reconcile, do not release the seats.
Hard problem 1: the hot show. A virtual queue gives each arriving user a position and admits them in batches; only admitted users receive a short-lived token that the hold endpoint requires. When the sold count hits 300, a flag in the cache flips the show to sold out and the remaining users are told at once.
Hard problem 2: pay after the hold expired. Extend the hold when the user reaches the payment step, with a cap. If a success still arrives after expiry, book the seats if they are free; if someone else took them, refund automatically and say so. Make this rare, but never make it impossible to handle.
Scaling later. Shard by show_id so a hold is always a single-shard transaction. Reads scale with the cache and replicas; writes stay on each primary.
Trade-offs
What you chose, and when you would flip.
- Conditional update vs
SELECT ... FOR UPDATE. I pick the conditional update: one round trip, short lock time, no way to forget to check. Flip to explicit locks when the rule needs to read several rows first, such as "no single empty seat left in a row". - Holds in the database vs a Redis key with a TTL. The database keeps holds and bookings in one transaction and one source of truth. A key with a TTL is faster and expires on its own, but now two stores must agree. Flip to Redis holds when hold attempts, even after admission control, exceed what a primary absorbs (past about 5,000 writes a second), and still write the final booking to the database.
- Short hold vs long hold. Five minutes returns unpaid seats to the pool quickly, but slow bank pages or wallet approvals will sometimes miss it. Flip to a longer hold only for the payment step, never for seat browsing.
- Waiting room vs plain rate limiting. A queue is fair and keeps the database calm but adds machinery and a bad first impression. For ordinary shows, a per-user rate limit is enough; for known hot launches, use the queue.
Follow-ups to expect
Where the interviewer pushes next.
- How would you stop a bot from grabbing all 300 seats with thousands of accounts?
- A payment succeeds 20 seconds after the hold expired and another user has the seat. What does the user see, and what do you do with the money?
- How would you let users choose the best available seats together in one row without locking the whole show?
- The database primary fails during the opening-night rush. What happens to in-flight holds?
- How would you support the same seat inventory being sold through a partner site as well as your own app?
Where candidates slip
The mistakes this question punishes.
- Reading the seat, checking in application code, then writing. Two users pass the check together and both are told they won.
- Treating the seat map as truth. A cached map is a hint, and a design that books from it will double-sell.
- Holds with no expiry, or an expiry that depends on a worker running on time. Put the expiry in the row and in the predicate.
- Releasing the hold when the payment call times out, then charging the card anyway.
- Sharding the seat table by seat or user. A multi-seat hold must be inside one shard.
- Sizing for average load and ignoring the 10,000-a-second burst on one show, which is the whole point of the question.
Have a go before you read.
Interviews reward the sketch you make cold. Clarify out loud, draw it, then compare with the answer.
Now make it stick
Where this was reported
Public pages only. Companies do not confirm these, and they rotate their questions, so treat each link as one person's account or one list's tag.