Understand it and size it: seats for a show
Part of the free System Design course on LogicWiz, in the chapter: Design ticket booking (BookMyShow). The interactive whiteboard exercises and simulations run in the app.
Week 17: Seats for a show
"Two people sat down in seat H7 at the same film. Both had a ticket. Both had paid."
A SwiftKart support agent, on the first complaint from the cinema pilot
Walk through one booking
SwiftKart is adding movie tickets. Kabir gives Meera the question: "Design the booking path for cinema seats." Before she draws a box, she walks through what one customer does. Each step may need a box later.
- Browse. The customer picks a city, then a film, then a theatre and a time. A show is one film, on one screen, at one time, such as "Screen 2, 7:30 pm".
- Pick seats. The app draws the seat map: the grid of seats in that screen, each free or taken. The customer taps H7 and H8.
- Hold. The system reserves those two seats for this customer for a few minutes. Nobody else can take them. A hold is a reservation that cancels itself when its time runs out.
- Pay. The customer pays by UPI. This takes 20 seconds or several minutes, depending on the bank app and the customer.
- Confirm. When the payment succeeds, the hold becomes a booking and the ticket arrives by SMS and email.
Meera's first idea was short: "A seat is a row. When the customer pays, insert a booking row for that seat." Kabir asked one question. "At 7:00 pm two customers both look at seat H7. What does each one see?" Both see it free. Both pay. Both insert a row. The gap between looking and paying is minutes long, and a plain insert does not guard it. The hold exists to close that gap.
Questions to ask before you draw
A strong candidate spends the first five minutes asking. Here is Meera's list, with Kabir's answers.
- Numbered seats or any seat? Numbered, picked on a map. That is why a hold must name specific seats.
- How many people? About 5 million customers a day, in about 500 cities.
- How long may a seat be held? 8 minutes. The payment session lasts 7 minutes.
- Is every day the same? No. When advance booking for a big film opens at a fixed time, such as 10:00, about 1 million people arrive within 2 minutes.
- May we sell more tickets than seats? No.
The last answer is worth a minute. Airlines sell more tickets than seats on purpose. This is called overbooking. Some passengers never turn up, and a passenger who is bumped can be paid and rebooked. A cinema has no such fix. A person without a seat has lost the film and the evening. So in this design overbooking is not allowed. A seat is sold once, and the database must enforce that.
Requirements with numbers
Functional, what the system does:
- Browse cities, theatres and shows, and open the seat map of a show.
- Hold the chosen seats for 8 minutes.
- Pay, then receive a confirmed booking and a ticket.
- Give unpaid or abandoned holds back to the free seats.
Non-functional, how well it does it:
- A seat map loads in under 300 ms for 99 of 100 requests (p99). It may be a few seconds old.
- No double booking, ever. Seat allocation is strongly consistent per show: every request sees the latest state of that show's seats.
- A hold lasts 8 minutes. The payment session lasts 7, so a payment cannot finish after the hold is gone.
- A booking is confirmed within 2 seconds of the payment succeeding (p99). The system is 99.95% available during the rush.
Out of scope: a customer cancelling a confirmed ticket, food orders, dynamic pricing and theatre onboarding.
Notice the split. Browsing may be slightly out of date. Booking may never be wrong. Almost every design choice later comes from that split. Now write the requirements yourself.
Size it: browsing, booking and the rush
Do three estimates, because the loads are different.
Browsing
5M customers x 10 page loads = 50M requests a day
50M / 86,400 s = ~580 per second on average
evening peak, 5 times = ~3,000 reads per second
Booking on a normal day
400,000 bookings a day / 86,400 s = ~5 per second
Friday evening, 10 times = ~50 bookings per second
Storage
8,000 theatres x 3 screens = 24,000 screens
24,000 x 5 shows = 120,000 shows a day
120,000 x 200 seats = 24M seat rows a day
open 7 days ahead: 7 x 24M = ~170M rows
170M x 50 bytes = ~8.5 GB
The rush (one big release at 10:00)
1M people, most in the first 30 s: 1M / 30 = ~33,000 requests per second
refreshes and retries, 3 times = ~100,000 requests per second
supply: 3,000 shows x 90 seats = 270,000 seats
payments over ~10 minutes: 270,000 / 600 s = ~450 callbacks per second
Now read the shape. At the evening peak, reads outnumber bookings about 60 to 1 (3,000 against 50), and 8.5 GB fits one database. The rush is different. Demand is several times supply, so most people will fail, and the system should tell them quickly. One popular show may draw 50,000 people for 90 seats.
Meera's first instinct was to size everything for 100,000 requests per second. Kabir corrected her. That number is the front door of the rush. The real limit is 90 seats on one show, because only one row of data can decide who gets H7. We split the database by show later, not for space, but so one hot show cannot slow all the others.
The API and the data model
GET /cities/{id}/shows?movieId=&date= -> 200 {theatres[{name, shows[]}]}
GET /shows/{id}/seats -> 200 {seats[{seatId, status}], asOf}
POST /holds {showId, seatIds[], idempotencyKey} -> 201 {holdId, expiresAt} | 409 seat taken
POST /holds/{id}/payment {method: "UPI"} -> 200 {paymentRef, payBy}
POST /payments/webhook {paymentRef, status, signature} -> 200
GET /bookings/{id} -> 200 {status, seats, ticket}
The field asOf tells the app how old the seat map is. A 409 (Conflict) means someone else took a seat first, and the app redraws the map. The idempotencyKey is a random id the app creates once per tap and reuses on every retry, so a retry on a bad network never makes two holds. The webhook is a call from the payment provider to our server. It is signed, so we can check it is real.
shows(show_id PK, movie_id, screen_id, starts_at)
seats(show_id, seat_id, status FREE | HELD | BOOKED, hold_id, held_until) PK (show_id, seat_id)
holds(hold_id PK, user_id, show_id, seat_ids[], status HELD | PAID | EXPIRED, expires_at, idempotency_key UNIQUE)
bookings(booking_id PK, hold_id UNIQUE, payment_ref UNIQUE, user_id, created_at)
The catalogue (cities, theatres, screens, films) sits beside these in plain tables. Seat rows are copied for every show when the show is created, because status belongs to the show: H7 at 7:30 is a different row from H7 at 10:30. The primary key on (show_id, seat_id) means a seat appears once per show. The unique columns let the database refuse duplicates: one hold per key, one booking per hold, one booking per payment.
📌 Summary: A seat moves from free to held to booked, and the hold closes the gap between looking and paying. Browsing may be stale, booking may not. Size three loads: browsing, normal booking, the rush.
What Meera learns next
The numbers say what to build: cheap cached reads, and a careful write path. Next, Meera draws the design from one box and adds each piece only when a number asks for it.
Check your understanding
- Meera's first design inserts a booking row when the customer pays. At 7:00 pm two customers both look at seat H7, both see it free, and both pay. What is missing?
- SwiftKart has 5 million customers a day and each makes about 10 page loads. Evenings are 5 times the average. About how many browse reads a second is the evening peak?
- Which sentence describes the right freshness for the two main screens?
- A teammate says: "Airlines overbook by a few percent because of no-shows. Let us sell 5% extra tickets for hot shows." What is the best reply?