System design deep dive · HLD
Design a Movie Ticket Booking System (BookMyShow-like): full high-level design.
Requirements, back-of-envelope capacity estimation, a high-level design diagram, a deeper deployment architecture diagram, sequence diagrams for concurrent seat locking and the payment-hold-with-timeout flow, and an entity-relationship diagram for the data model - with the reasoning an interviewer expects behind every box and arrow.
1. Clarify requirements before drawing any box
Ticket booking is a concurrency problem wearing a UI: hundreds of people can look at the exact same seat map for a blockbuster's opening show at the same second, and only one of them can end up with seat A12. Every requirement below exists to make that guarantee cheap to enforce.
Functional requirements
Non-functional requirements
2. Back-of-the-envelope capacity estimation
These numbers decide why seat locking has to be a fast in-memory operation rather than a database row lock held across a whole checkout, and how big the reaper job's workload is.
| Metric | Assumption | Resulting estimate |
|---|---|---|
| Shows running | 50,000 theatres × ~6 shows/day | ~300,000 active shows/day nationally |
| Seats per show | ~150 seats average | ~45 million bookable seat-slots/day |
| Normal checkout concurrency | Steady-state booking traffic | ~50 concurrent checkouts/sec system-wide |
| Flash-sale spike (blockbuster release) | 50-100x normal for one show's on-sale moment | ~2,500-5,000 concurrent lock attempts/sec on a single show's ~150 seats |
| Lock store size | Every held-but-unpaid seat, TTL 10 minutes | Worst case ~1M concurrently-locked seats × ~100 bytes ≈ 100 MB - trivial for Redis |
3. High-level design (HLD)
The HLD names the major components and the one-directional data flow between them, without yet committing to region layout, replica counts, or how the reaper job is scheduled - that belongs in the architecture diagram in the next section.
What each box owns
Seat inventory service
The single authority for a seat's state (available/locked/booked) for a given show. It performs the lock as one atomic conditional write against the lock store - "set this seat to locked by session S with a 10-minute TTL, only if it isn't already locked or booked" - and rejects the request outright if it isn't. It is the only service allowed to transition a seat's state.
Seat lock store (Redis)
Holds one key per (show_id, seat_id) with the holding session id as the value and a TTL equal to the checkout window. A conditional SET key value NX EX 600 is what makes the lock atomic under massive concurrency - only one of thousands of simultaneous requests for the same seat can ever succeed, and the rest fail fast instead of queueing.
Booking service
Orchestrates the checkout: confirms the lock is held by the current session, calls the payment service, and on success writes the durable booking row and flips the seat's state from "locked" to "booked" in the same transaction. On payment failure or lock expiry, it does nothing to the booking table - there's simply no booking to roll back, because the lock was never converted to a booking.
Reaper job and search cache
Redis's own TTL expiry already frees an abandoned lock automatically, so the "reaper" is mostly a safety-net background job that reconciles any lock/booking-table inconsistency and re-publishes updated seat-availability counts to the search cache, which is what lets the search service answer "how many seats left" without hitting the lock store on every search request.
4. Detailed architecture diagram
The architecture diagram takes every HLD box and answers "how does this survive a single blockbuster's ticket-release moment without falling over?" - the scenario every interviewer for this problem eventually asks about.
| Decision | Choice | Reasoning |
|---|---|---|
| Seat locking mechanism | Atomic conditional write in Redis (SET NX EX), not a DB row lock | Under 5,000 concurrent attempts/sec on one show, a database transaction holding a row lock would serialize and time out requests; an in-memory conditional write resolves each attempt in sub-millisecond time. |
| Flash-sale traffic control | Virtual waiting room in front of the API gateway | Admits users into the actual booking flow at a controlled rate instead of letting all simultaneous requests hit the seat inventory service at once, smoothing the spike without rejecting users outright. |
| Bookings DB sharding key | Hash of show_id | A seat only ever needs to be checked against other seats in the same show; sharding by show keeps every booking transaction single-shard. |
| Read/write path separation | Search service and its replicas scaled independently from the locking path | Browsing showtimes is read-heavy and cacheable; it must never compete for the same capacity as the latency-critical, write-heavy seat-lock path. |
5. Sequence diagrams for the two critical flows
A sequence diagram is where an interviewer checks whether the lock is acquired atomically before payment even starts, and whether an unpaid lock is guaranteed to expire without a background process being strictly required for correctness.
5.1 Concurrent seat locking during checkout
Step 3's single atomic conditional write is the entire mechanism - there is no separate "check if free" call followed by a "lock it" call, because that two-step pattern is exactly the race condition that would let both A and B believe they got the seat. Whoever's SET NX reaches Redis first wins; the loser is told immediately, in milliseconds, not after a queued wait.
5.2 Payment hold with timeout releases unpaid seats
The correctness of the release does not depend on any job running - Redis's own TTL mechanism deletes the key at t+600s regardless of whether the reaper (step 5) has run yet. Step 5 is only there to keep the seat-map cache and search counts fresh for other browsing users; if the payment service tries to confirm a booking against an already-expired lock (step 6), the booking service's own re-check of the lock's ownership catches it and rejects the confirmation.
6. Entity-relationship (ER) diagram and schema
The data model has to answer three questions: what durably represents a confirmed booking versus a transient hold, how is a theatre's physical seat layout reused across many shows, and how is double-booking prevented at the database layer as a second line of defense behind the Redis lock.
Key modeling decisions
| Storage choice | Use when | Watch out for |
|---|---|---|
| Relational (Postgres/MySQL), sharded by show_id | You need the UNIQUE(show_id, seat_id) constraint as a hard backstop against double-booking, and straightforward per-show reporting. | A single wildly popular show's write volume can still hot-spot one shard; the Redis lock in front absorbs almost all of that contention before it reaches the DB. |
| NoSQL (DynamoDB) with conditional writes | You want the "only if not already booked" guarantee enforced by a conditional PutItem instead of a relational unique constraint. | Cross-seat queries (seat-map rendering for a whole show) need a well-chosen partition key (show_id) to stay efficient. |
7. Deep dives interviewers actually probe
Optimistic vs pessimistic locking - which one and why?
Pessimistic locking (the Redis SET NX approach used here) blocks a second attempt immediately, which is the right trade-off when contention is expected to be high and short-lived, exactly the flash-sale scenario this system is built for. Optimistic locking (read seat version, attempt to book, retry on version-mismatch conflict) works better under low contention but degrades badly under high contention because most attempts fail late, after doing more work - the wrong shape for a blockbuster's on-sale moment.
How do you handle a flash sale for a blockbuster's opening show without the seat inventory service falling over?
A virtual waiting room admits users into the actual booking flow at a rate the backend can sustain, issuing a short-lived admission token rather than letting every one of 100,000 simultaneously-refreshing users hit the seat inventory service at once. Everyone still gets a fair, ordered shot at the seats; the system just controls the rate of contention instead of trying to absorb an unbounded spike.
What if the Redis lock store itself fails or a lock is lost mid-checkout?
This is exactly why UNIQUE(show_id, seat_id) exists on the durable bookings table as well - if Redis loses a key early (a restart, a rare failover edge case) and two customers both reach the "write the confirmed booking" step for the same seat, the database's unique constraint rejects the second insert and that customer's payment is refunded. The Redis lock is an optimization that makes the common case fast and cheap; the database constraint is what makes the invariant actually unbreakable.
Why a fixed 10-minute checkout window instead of something dynamic?
A fixed, generous-but-bounded window is simple to reason about and to communicate to the user ("seats held for 10 minutes"), and it caps the worst case of how long a seat can be unavailable to others due to one abandoned session. A dynamic window (e.g., shorter during a flash sale) is a reasonable refinement but adds complexity - it's called out as a tunable parameter rather than a structural change to the locking mechanism itself.
How does showtime search stay fast when seat availability is changing constantly underneath it?
Search never queries the lock store directly - it reads a periodically-refreshed "seats remaining" counter from its own cache, updated by the reaper/reconciliation path and by booking confirmations, which is allowed to be a few seconds stale. The seat map for a specific show the user has actually opened does read live lock state, because that's the one screen where staleness would show a seat as available that's actually held by someone else mid-checkout.
Post a Comment
Add