Scope it first
"Browse movies by city, pick a show, see the seat map, hold seats, pay, get a ticket. One city's catalog, ignore payments beyond success/failure callbacks, ignore pricing tiers — OK?" And know what you're walking into: every other classic hides its concurrency question at the end — BookMyShow is the concurrency question. Two users, one seat, same second; everything else is supporting cast.
Entities & relationships
The catalog is a ladder of containment — get the chain right and the rest follows:
The non-obvious modeling decision — the one graders watch for: a Seat
is physical and permanent; its availability belongs to the Show.
Screen 4's seat F7 exists once; whether it's free is a different fact for
the 6 PM and 9 PM shows. So Show owns
Map<Seat, SeatState> where SeatState = AVAILABLE | LOCKED | BOOKED.
Hanging state on Seat itself is the classic mistake here.
BookingStatus: PENDING → CONFIRMED | EXPIRED | CANCELLED — a small
state machine again, because payment isn't instant.
The flow: lock → pay → confirm
Booking can't be atomic — payment takes a minute and might fail — so the flow is two-phase with a TTL:
- Lock. User picks seats → atomically transition each from
AVAILABLE → LOCKED(user, expiresAt = now + 7 min). Any seat unavailable → whole request fails (all-or-nothing; nobody wants half a family's seats). - Pay. A
PENDINGBooking exists; user completes payment with the gateway. - Confirm or release. Payment success →
LOCKED → BOOKED, bookingCONFIRMED. Failure or timeout → lock expires, seats return toAVAILABLE, bookingEXPIRED.
The TTL is the design's quiet hero: no user action is ever required to
free a seat — abandonment self-heals. Expiry via lazy check
(isLocked() = state == LOCKED && now < expiresAt) plus a sweeper; lazy
checking means correctness never depends on the sweeper being on time.
Drive a single seat through that machine. Run the happy path lock → pay, then
try the abandonment path (lock, then expire — the TTL returning the seat for
free), and the illegal pay before lock — which is refused, because you can't
pay for a seat you never held:
1/4Start in Free. Each event is handled by the current state — the State pattern moves this branching out of one giant switch and into the state objects themselves.
Here's the whole two-phase flow with the race resolving correctly — two users, one pair of seats, 80 ms apart:
The race, and the only acceptable answer
Two users hit "lock F7" in the same 10 ms. A read-then-write
(if seat is available → mark locked) lets both see AVAILABLE and
both proceed — double-booking, the cinema equivalent of the
parking lot's last spot and a balance double-spend.
The invariant must be enforced at the point of mutation:
class ShowSeating:
def lock_seats(self, seats, user):
with self.lock: # one mutex per SHOW — not global
for s in seats: # check ALL first…
if not self.is_available(s):
raise SeatUnavailable(s)
for s in seats: # …then mutate (all-or-nothing)
self.states[s] = Locked(user, now() + LOCK_TTL)
return Booking(pending, seats, user)
In a database (the production reality), same idea, different spelling — one atomic conditional write:
UPDATE show_seats SET state = 'LOCKED', user_id = :u, expires_at = :t
WHERE show_id = :s AND seat_id IN (:seats)
AND (state = 'AVAILABLE' OR (state = 'LOCKED' AND expires_at < now()));
-- affected rows < requested seats ⇒ roll back, tell the user
Lock granularity is the follow-up: a global lock serializes the whole country's bookings; per-show locking is natural (contention is per-show by definition) and shows you think about lock scope.
Where the patterns live
- State — seat availability and booking lifecycle; illegal transitions
(
BOOKED → LOCKED) become unrepresentable. - Strategy — seat recommendation ("best available": center-out, contiguous-block-finding) and later pricing.
- Observer — seat-map UIs subscribe to seat-state changes (everyone watching the map sees F7 grey out live); notification service subscribes to booking confirmations.
- Facade —
BookingService.book(show, seats, user)orchestrating lock → payment → confirm, so controllers never touch seat maps.
The sentence graders wait for: "The invariant is a seat is never LOCKED or BOOKED by two users for the same show; I enforce it with an atomic check-and-set under a per-show lock, with TTL so abandoned locks self-release." Mechanism without the invariant sounds memorized; invariant first proves you know why.
Think it through like the interview
PROBLEMDesign movie-ticket booking: browse shows, view a seat map, hold seats, pay, get a ticket. Two users will inevitably grab the same seat in the same second.
- 1
Identify what's REALLY being asked
“Every classic has one core test. What is BookMyShow actually grading?”
- 2
The entity ladder + the trap
“City → Cinema → Screen → Seat… so where does 'is F7 free?' live?”
unlocks after the stage above - 3
Design around the slow step
“Payment takes a minute and might fail. What does that force on the flow?”
unlocks after the stage above - 4
Kill the race at the mutation point
“Two lock requests, 10 ms apart, both read F7 as AVAILABLE. Where's the bug, and where's the fix?”
unlocks after the stage above - 5
Scale the answer
“Follow-up: blockbuster tickets open at 9 AM. The per-show lock melts. Now what?”
unlocks after the stage above
Walk a scenario
Asha and Rahul, both eyeing F7+F8 for the 9 PM show. Asha's lock request
wins the mutex: both seats → LOCKED(asha), PENDING booking, 7-minute
clock. Rahul's request enters the mutex 80 ms later, sees F7 LOCKED and
unexpired → SeatUnavailable; UI offers the Strategy's next-best
contiguous pair. Asha pays in 3 minutes → seats BOOKED, booking
CONFIRMED, observers repaint the map. Had she abandoned: at minute 7 the
seats lazily read as AVAILABLE again — no cleanup dependency, no stuck
inventory.
Practice — level up
BookMyShow is reserve under contention with a TTL. Drill the allocation and the expiry separately, then together:
Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.
Warm-up — reserve a specific slot
- Seat Reservation ManagerMedium
Reserve the next free seat, release on cancel — the allocation core.
Core — expiry & time-versioned state
Tokens with a TTL — exactly the seat-lock that self-heals on timeout.
State versioned by time — reason about lock expiry windows.
Stretch — paired entry/exit
Check-in/check-out pairs and charge on completion.