Movie Ticket Booking Interview Questions | JiQuest

add

#

Movie Ticket Booking

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.

200Concurrent checkouts / hot show
10minSeat lock timeout
0Tolerable double-bookings
Moviegoerpicks seat A12 Booking svctries to lock seat Redis lockSET NX, TTL 10min Seat lockedproceed to payment Reaper jobreleases on timeout 10-min countdown starts

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

Search showtimesFind shows by movie, city, theatre, and date/time with live seat-availability counts.
View seat mapShow a theatre's seat layout for a specific show with per-seat status (available/locked/booked).
Lock & pay for seatsTemporarily hold selected seats during checkout, then confirm the booking once payment succeeds.
Auto-release on timeoutSeats locked but not paid for within the checkout window are released back to availability automatically.

Non-functional requirements

Zero double-bookingTwo customers must never both end up holding a confirmed ticket for the same seat/show.
Handle flash-sale spikesA blockbuster's ticket-release moment can produce a 50-100x normal traffic spike in seconds.
Low seat-map read latencyThe seat grid is read far more than it's written; it must render fast even under load.
Bounded lock durationA seat cannot be held indefinitely by an abandoned checkout - locks must expire deterministically.
Explicitly out of scope Dynamic pricing based on demand, loyalty/rewards point redemption, and a virtual-waiting-room queueing UI for ticket-release moments are named as extensions in the deep-dive section so the core seat-locking loop stays the focus.

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.

MetricAssumptionResulting estimate
Shows running50,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 concurrencySteady-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 sizeEvery held-but-unpaid seat, TTL 10 minutesWorst case ~1M concurrently-locked seats × ~100 bytes ≈ 100 MB - trivial for Redis
Why this matters The flash-sale row is the number that actually drives the design: 5,000 lock attempts/sec contending for 150 seats means the lock operation itself must be a single atomic, sub-millisecond in-memory operation, because a database transaction with row-level locking under that much contention would queue up and time out requests.

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.

Moviegoer appsearch + book API gatewayrate limit / queue Search serviceread-heavy, cached Seat inventory svclock / release / confirm Booking serviceorchestrates checkout Search cache (Redis) Seat lock storeRedis, TTL per seat Bookings DBsharded by show_id Payment serviceholds funds on file Reaper jobexpired lock cleanup
Stateless servicesFast-path infraDurable storageAsync / edge

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.

Edge layer CDN (posters, static) Virtual waiting roomtoken-gated entry, per hot show API gateway + WAF Rate limiterper-session, per-show Hot path: seat locking Seat inventory svc ×12 pods Booking svc ×8 pods Lock store shard (per show) Seat-map cache Search / read path (separately scaled) Search svc ×20 pods Read replicas (showtimes) isolated from the write-heavy locking path so a search spike never slows a checkout Bookings storage tier Shard: shows 0-N/2 Shard: shows N/2-N sharded by show_id - never split across a show Payment + reaper Payment svc Reaper (cron) reconciles lock-store vs bookings table Notification E-ticket + reminder dispatch async, on booking.confirmed event
DecisionChoiceReasoning
Seat locking mechanismAtomic conditional write in Redis (SET NX EX), not a DB row lockUnder 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 controlVirtual waiting room in front of the API gatewayAdmits 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 keyHash of show_idA 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 separationSearch service and its replicas scaled independently from the locking pathBrowsing 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

Customer A Customer B Seat inventory svc Lock store (Redis) 1. lock seat A12 (session s1) 2. lock seat A12 (session s2) 3. SET A12 s1 NX EX 600 (both sent nearly together) 4. OK (s1 wins - key didn't exist) 5. SET A12 s2 NX → already exists, fails 6. 200 locked, proceed to payment 7. 409 seat unavailable, pick another

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

Customer Booking svc Lock store Payment svc 1. lock confirmed, checkout timer starts 2. customer abandons checkout - closes tab 3. Redis TTL expires at t+600s 4. key A12 auto-deleted (no explicit release call needed) 5. reconcile: seat_state = available 6. late payment attempt fails - lock gone 7. 410 hold expired, re-select seat

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.

theatres PK id BIGINT name, city VARCHAR screens INT lat, lng DOUBLE shows PK id BIGINT FK theatre_id BIGINT movie_id BIGINT screen_no INT start_time TIMESTAMP base_price BIGINT seats PK id BIGINT FK theatre_id BIGINT row_label, seat_no VARCHAR/INT tier ENUM(silver,gold,premium) bookings PK id BIGINT FK show_id BIGINT FK seat_id BIGINT UQ (show_id, seat_id) 1N 1N 1N 1N seats belong to a theatre (physical layout, reused across many shows on that screen); a booking pins one seat to one show via a unique (show_id, seat_id) constraint

Key modeling decisions

seats is per-theatre, not per-showA theatre's physical seat layout (row/number/tier) is fixed and reused across every show on that screen; only bookings ties a seat to one specific show.
UNIQUE(show_id, seat_id) on bookingsEven if the Redis lock is somehow bypassed or the reaper is behind, the database itself physically cannot store two confirmed bookings for the same seat/show - this is the second line of defense.
No "locked" row exists in the durable DBA hold is purely a Redis key; the bookings table only ever gains a row once payment succeeds, so an abandoned checkout leaves zero durable trace to clean up.
price is captured on the booking, not looked up laterBooking stores the paid amount at confirmation time, independent of any later change to the show's base_price or dynamic pricing.
Storage choiceUse whenWatch out for
Relational (Postgres/MySQL), sharded by show_idYou 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 writesYou 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.

8. Summary: what a strong answer covers

Atomic conditional lock, not check-then-setTTL-based release needs no explicit unlock callDB unique constraint as a second line of defense Waiting room to smooth flash-sale spikesSearch path scaled independently from locking pathPessimistic locking justified by expected contention
Interview tip When asked to design a ticket booking system, the strongest signal is naming the exact atomic primitive that prevents double-booking (a conditional write, not a read-then-write) and explaining why the checkout window's expiry doesn't depend on any background job to be correct.
No comments
Leave a Comment