Movie Ticket Booking (BookMyShow) — LLD
Problem statement
Design the object model and seat-locking logic for a ticket booking system: cities, theatres, screens, shows, seats; a user selects seats for a show, seats are temporarily locked, payment completes the booking, and locks expire if unpaid. No two users may book the same seat.
Requirements
Functional
Browse shows by city/movie/theatre; view seat availability for a show.
Lock selected seats for a user for N minutes; confirm after payment; release on timeout/cancel.
Booking history; cancellation with refund policy.
Non-functional
Correct under concurrent lock attempts (no double booking).
Lock provider pluggable (in-memory, DB, Redis).
Clear separation of catalogue vs booking concerns.
API
List<Show> ShowService.getShows(cityId, movieId, date)
List<ShowSeat> SeatAvailabilityService.getAvailableSeats(showId)
Booking BookingService.createBooking(userId, showId, List<seatId>) // locks seats, PENDING
Booking BookingService.confirm(bookingId, paymentDetails) // validates locks, BOOKED
void BookingService.cancel(bookingId)
Data model
City,Theatre(city, screens),Screen(theatre, seats),Seat(id, row, col, SeatType).Movie(id, title, duration),Show(id, movie, screen, startTime, pricing).ShowSeat(show, seat, status: AVAILABLE|LOCKED|BOOKED, lockedBy, lockedAt)— the per-show projection of a physical seat.SeatLockProvider(interface):lockSeats(show, seats, user),unlockSeats,validateLock(show, seat, user),getLockedSeats(show).Booking(id, show, user, seats, status: PENDING|CONFIRMED|CANCELLED, amount).PaymentandPaymentStrategy.- Services:
TheatreService,ShowService,BookingService,PaymentService,SeatAvailabilityService.
Design
- Lock-then-pay:
createBookingasksSeatLockProvider.lockSeatsatomically for all requested seats; if any is locked/booked by someone else, fail the whole request (all-or-nothing). - In-memory lock provider:
Map<Show, Map<Seat, SeatLock{user, expiry}>>guarded by a lock per show (synchronizedon the show's map or aReentrantLockper show). Expired locks are treated as free when checked. - Confirm: validate every seat is still locked by this user and not expired → mark
BOOKEDin the show's seat map → bookingCONFIRMED→ release locks. - Expiry: a scheduler sweeps expired locks, or expiry is lazy (checked on access). With Redis:
SET key user NX PX ttlper seat plus a Lua script for all-or-nothing. - State pattern on
Booking: transitions validated (PENDING → CONFIRMED | CANCELLED,CONFIRMED → CANCELLEDwith refund rules). - Catalogue services are read-mostly and independent; only
BookingServicetouches seat state.
Class diagram
Deep dives
Atomic multi-seat lock: check all, then lock all, under one lock per show; otherwise two users can each get half of a pair.
Why ShowSeat exists: a physical Seat belongs to a screen; its availability is per show — never store status on Seat.
Lock ownership: validating lockedBy == user at confirm prevents a user from confirming seats another user locked after theirs expired.
Pricing: Show carries a Map<SeatType, Money>; booking amount computed at lock time and stored.
Bottlenecks & mitigations
Lock per show serialises all users of a hot show → acceptable (operations are microseconds); shard by show across servers.
Expired-lock sweeping → lazy checks on access plus a periodic cleaner.
In-memory provider is single-node → swap for Redis/DB provider via the interface.
Failure modes
- Payment success but confirm fails (lock expired due to slow payment) → extend locks while payment is in progress; else refund and inform.
- Process crash → in-memory locks lost; a persistent provider (Redis TTL/DB) is required in production.
- User closes the app → locks expire naturally.
- Cancellation after show start → policy check in
cancel(state + time rule).
Code skeleton
import time
from threading import Lock
from collections import defaultdict
class SeatLock:
def __init__(self, user, ttl): self.user, self.exp = user, time.time() + ttl
def expired(self): return time.time() > self.exp
class InMemorySeatLockProvider:
def __init__(self, ttl=600):
self.ttl, self.locks, self.mutex = ttl, defaultdict(dict), defaultdict(Lock)
def lock_seats(self, show, seats, user):
with self.mutex[show]:
held = self.locks[show]
for s in seats:
lk = held.get(s)
if lk and not lk.expired() and lk.user != user:
raise Exception(f"seat {s} unavailable")
for s in seats: held[s] = SeatLock(user, self.ttl)
def validate(self, show, seat, user):
lk = self.locks[show].get(seat)
return bool(lk) and not lk.expired() and lk.user == user
def unlock(self, show, seats, user):
with self.mutex[show]:
for s in seats:
lk = self.locks[show].get(s)
if lk and lk.user == user: del self.locks[show][s]
class BookingService:
def __init__(self, lock_provider):
self.lp, self.booked, self.bookings, self.mutex = lock_provider, defaultdict(set), {}, defaultdict(Lock)
def create(self, user, show, seats):
with self.mutex[show]:
if any(s in self.booked[show] for s in seats): raise Exception("already booked")
self.lp.lock_seats(show, seats, user)
b = {"id": len(self.bookings)+1, "user": user, "show": show, "seats": seats, "status": "PENDING"}
self.bookings[b["id"]] = b; return b
def confirm(self, bid, pay_ok=True):
b = self.bookings[bid]
if not pay_ok: raise Exception("payment failed")
with self.mutex[b["show"]]:
if not all(self.lp.validate(b["show"], s, b["user"]) for s in b["seats"]):
raise Exception("lock expired")
self.booked[b["show"]].update(b["seats"]); b["status"] = "CONFIRMED"
self.lp.unlock(b["show"], b["seats"], b["user"])
return b
Follow-up questions interviewers ask
Add seat categories with different hold times.
Support group bookings that must be adjacent.
How does this map to the HLD (DB rows, Redis, waiting room)?