Design a Movie Ticket Booking System
BookMyShow — theatres, shows, seat maps, and concurrent seat locking with a payment workflow.
Problem Statement
Design the object model for a movie ticket booking system like BookMyShow. Customers browse cinemas, screens, and shows, inspect a seat map with seat types, temporarily hold seats, pay for the booking, and then receive a confirmed ticket.
The hard part is not listing movies. The hard part is preserving the invariant that one physical seat in one show can be sold to at most one booking, even when two users click the same seat at nearly the same time.
Business context
Movie booking is a favorite intermediate LLD problem because it combines domain modelling with a real concurrency race. A candidate must model cinemas, screens, shows, seats, seat types, holds, payments, and booking lifecycle without drifting into high-level distributed systems.
Interviewers expect a clean state machine for bookings, a thread-safe seat locking provider with expiry, strategy seams for pricing and seat allocation, observer hooks for seat availability updates, and a small controller facade that keeps the flow easy to reason about.
Functional Requirements
Support cinemas with screens, and screens with scheduled shows for a movie.
Model seats per show because the same physical seat is available independently across different shows.
Represent seat types such as SILVER, GOLD, and PLATINUM, with different prices.
Let a customer select specific seats or let the system allocate seats using a strategy.
Place a temporary hold on selected seats before payment, with a timeout.
Prevent two customers from holding or booking the same seat for the same show concurrently.
Move bookings through Created, SeatsHeld, Confirmed, Expired, and Cancelled states.
Confirm a booking only after payment succeeds and the seat hold is still valid.
Notify availability observers when seats are held, released, expired, or booked.
Non-Functional Requirements
Correctness under concurrency
The check for seat availability and the write of the hold must be atomic. If User A and User B race for A1, exactly one hold can win.
Short-lived holds
Seats should not remain blocked forever if a customer abandons checkout. Holds expire after a configured duration and seats return to availability.
Extensibility
New pricing rules, allocation policies, payment providers, and notification channels should be additive classes, not edits to the core booking flow.
Low latency for seat selection
Seat map reads and hold attempts should be in-memory and fast for an interview design. A production system can persist the same aggregate with row locks.
Auditable lifecycle
The booking state machine should make every transition explicit so expiry, cancellation, and confirmation can be traced and tested.
Requirement Clarification
QIs a seat tied to a cinema screen or to a show?
The physical seat belongs to a screen, but availability belongs to a show. The same A1 can be booked for a 10 AM show and remain free for a 1 PM show.
QCan users hold seats without paying immediately?
Yes. A booking starts in Created, moves to SeatsHeld after the lock succeeds, and then becomes Confirmed only after successful payment.
QWhat happens when the hold timeout expires during checkout?
The booking becomes Expired, its seat locks are released, and any later payment attempt must fail or be refunded by the payment workflow.
QDo we need to model movie search, offers, coupons, or food ordering?
Not in the core design. Keep the scope to shows, seats, holds, booking lifecycle, pricing, and payment. Coupons and snacks are extensions.
QShould the design support manual seat choice and auto-allocation?
Yes. Manual choice goes through holdSpecificSeats; auto-allocation is a pluggable SeatAllocationStrategy.
UML Class Diagram
Sequence Diagram
Entity Identification
Cinema
Venue that owns screens. The implementation keeps cinema details on Show for brevity, but the model can split it into a first-class aggregate.
Screen
Auditorium inside a cinema. It owns the physical seat layout; each scheduled show receives its own availability view.
Show
A movie scheduled on a screen at a time. Owns show-specific seats, exposes availability snapshots, and notifies observers on seat changes.
Seat
Represents one seat for one show. Guards status transitions from AVAILABLE to HELD to BOOKED, and remembers the booking that currently holds it.
Booking
Customer checkout session. Holds selected seat ids, amount, payment status, and a BookingState object that enforces legal transitions.
SeatLockProvider
Thread-safe lock registry keyed by show id and seat id. It atomically grants holds, validates holds before confirmation, and releases expired locks.
PricingStrategy
Calculates the payable amount for selected seats. The base implementation prices by seat type; surge, weekday, coupon, and member pricing are extensions.
SeatAllocationStrategy
Chooses seats when the customer asks for automatic allocation. The demo picks sorted seats of a preferred type, but the interface supports better policies.
BookingService
Singleton controller facade for registering shows, holding seats, confirming payment, expiring holds, and cancelling bookings.
Design Patterns Used
BookingState makes lifecycle rules explicit. Created can hold, SeatsHeld can confirm, expire, or cancel, and terminal states reject invalid transitions.
PricingStrategy varies price calculation and SeatAllocationStrategy varies seat choice. The booking flow asks interfaces instead of hard-coding policies.
SeatAvailabilityObserver lets UI boards, analytics, or notifications react when seats are held, released, expired, or booked without coupling them to Show.
BookingService.getInstance exposes a single in-memory controller for the demo so every thread competes against the same booking and lock maps.
Step-by-Step Design
1Make show seats independent from physical seats
A screen has a physical layout, but each Show needs its own seat availability. Model seats inside the show instance so A1 for the morning show is independent from A1 for the evening show.
2Guard seat status inside the seat object
Seat owns the invariant that an available seat can become held, a held seat can become booked by the same booking, and an unrelated booking cannot steal it.
public synchronized void hold(String bookingId) { if (status != SeatStatus.AVAILABLE) { throw new IllegalStateException("Seat is not available"); } status = SeatStatus.HELD; this.bookingId = bookingId; }3Put the race in one synchronized lock provider
The critical section is check-then-hold. SeatLockProvider.lockSeats synchronizes the entire operation, removes expired holds first, rejects already-held or booked seats, and only then writes the hold.
public synchronized boolean lockSeats(Show show, List<String> seatIds, String bookingId) { cleanupExpired(show); for (String seatId : seatIds) { if (show.getSeat(seatId).getStatus() != SeatStatus.AVAILABLE) { return false; } } show.holdSeats(seatIds, bookingId); return true; }4Represent booking lifecycle with State
A booking starts in Created. A successful lock moves it to SeatsHeld. Payment success confirms it; timeout expires it; user abandonment cancels it.
public synchronized void confirm() { if (paymentStatus != PaymentStatus.SUCCESS) { throw new IllegalStateException("Payment must succeed before confirmation"); } state.confirm(this); }5Separate policies from the booking flow
PricingStrategy decides the amount and SeatAllocationStrategy decides which seats to try. The service remains focused on orchestration and concurrency.
6Confirm only while the hold is still valid
During confirmation, the service charges payment, validates the lock again, consumes the lock, marks seats booked, and then transitions the booking to Confirmed.
7Notify availability after every seat change
Show calls observers with an availability snapshot after hold, release, expiry, or booking. This keeps UI refresh concerns out of the domain flow.
Complete Java Implementation
Explanation of Every Class
Seat
Encapsulates one show-seat and its status. All status-changing methods are synchronized so HELD and BOOKED transitions cannot be corrupted by concurrent callers.
Show
Owns show-specific seat objects, exposes availability snapshots, performs seat status changes, and notifies SeatAvailabilityObserver subscribers after each change.
Booking
Carries customer, show, seat ids, amount, payment status, and the active BookingState. The state classes enforce Created to SeatsHeld to Confirmed, Expired, or Cancelled.
SeatLockProvider
Thread-safe hold registry. Its synchronized methods clean expired holds, atomically check every requested seat, write holds with expiry, validate holds, and release or consume them.
PricingStrategy
Contains the pricing and allocation seams: PricingStrategy with SeatTypePricingStrategy, plus SeatAllocationStrategy with ContiguousSeatAllocationStrategy.
BookingService
Singleton controller facade. It registers shows, creates bookings, asks allocation and pricing strategies, delegates concurrency to SeatLockProvider, and coordinates payment confirmation.
Main
Executable demo. Two threads attempt to hold the same seat A1; the synchronized lock provider lets one booking proceed and makes the other fail cleanly.
Dry Run
Sample input
Show SHOW-1 has seats A1:GOLD, A2:GOLD, and B1:SILVER. User A and User B both try to hold A1 at the same time, then the winner pays.
| Step | User A | User B | Seat A1 | Booking states | Outcome |
|---|---|---|---|---|---|
| 1 | Reads A1 as available | Reads A1 as available | AVAILABLE | none | Both clients can see the same snapshot |
| 2 | lockSeats enters first | waits for lock provider | HELD by A | A: SeatsHeld | Atomic check-then-hold succeeds for A |
| 3 | continues checkout | lockSeats enters second | HELD by A | A: SeatsHeld | B is rejected because A1 is no longer AVAILABLE |
| 4 | payment succeeds | receives failure | HELD by A | A: SeatsHeld | B has no booking to confirm |
| 5 | consume lock and confirm | no-op | BOOKED by A | A: Confirmed | A1 is sold exactly once |
The important row is Step 2: both users may observe the same available seat, but only one thread can execute the synchronized hold mutation at a time.
Complexity Analysis
| Operation | Time | Space | Note |
|---|---|---|---|
| holdSpecificSeats | O(K) | O(K) | K selected seats are validated, priced, locked, and stored in the booking. |
| createAndHoldBooking | O(S log S + K) | O(S) | The demo allocation strategy sorts available seats of the requested type before choosing K seats. |
| confirmBooking | O(K) | O(1) | Validates K locks, consumes them, marks K seats booked, and transitions the booking. |
| availabilitySnapshot | O(S) | O(T) | Scans S seats and counts them by T seat types. |
| cleanupExpired | O(H) | O(E) | Scans H active holds for a show and releases E expired seats. |
For an interview, O(S) seat scans are acceptable for one screen. At scale, maintain per-show free-seat indexes and move hold cleanup to a scheduler or database TTL mechanism.
Extensibility
New pricing model
Add a PricingStrategy for weekend surge, early-bird discounts, coupon stacking, membership benefits, or movie-specific pricing.
Better seat allocation
Replace ContiguousSeatAllocationStrategy with center-first, group-adjacent, aisle-preference, or accessibility-aware allocation.
Payment providers
Swap PaymentGateway implementations for Razorpay, Stripe, wallet balance, or UPI. The booking lifecycle stays unchanged.
Real-time UI updates
Attach observers that publish WebSocket events whenever Show emits held, released, expired, or booked events.
Persistence
Move shows, bookings, and holds behind repositories. The same lock boundary can become a database transaction with row-level locks on show-seat rows.
Alternative Designs
Database row lock per show seat
Represent each show seat as a row and use a transaction with SELECT FOR UPDATE or an atomic compare-and-set update for HELD and BOOKED transitions.
Tradeoffs
This works across multiple app instances and survives restarts, but it introduces database contention and transaction timeout handling.
Distributed lock per show-seat key
Use Redis or ZooKeeper style locks keyed by show id and seat id, with TTL equal to the hold timeout.
Tradeoffs
Good for horizontally scaled services, but lock loss, clock drift, and payment timing need careful compensating logic.
Optimistic booking with version numbers
Let users attempt to book based on a seat version and update only if the version is unchanged.
Tradeoffs
Reduces lock duration and improves read throughput, but users see more payment-time failures if seats are popular.
Common Mistakes
- ×
Treating screen seats as globally booked instead of modelling show-specific availability.
- ×
Checking availability in one method and writing the hold in another without a shared lock or transaction.
- ×
Confirming payment before verifying that the seat hold still belongs to the same booking.
- ×
Using a boolean isBooked flag and forgetting the important HELD state.
- ×
Allowing cancellation or expiry after Confirmed without defining a refund state.
- ×
Hard-coding prices in the controller instead of using a PricingStrategy.
- ×
Not releasing holds when payment fails, leaving seats blocked until manual cleanup.
Follow-up Interview Questions
QHow do you handle two users selecting the same seat at the same time?
Both can read the same snapshot, but only one can enter SeatLockProvider.lockSeats at a time. The winner writes a hold; the loser sees HELD or an existing hold and fails.
QWhat if the payment succeeds but the hold has expired?
The service validates the hold before confirmation. In production, the payment flow should authorize first, capture only after seat confirmation, or issue an automatic refund on late expiry.
QWhy not directly mark seats booked when the user clicks them?
Checkout can be abandoned or payment can fail. A temporary HELD state blocks the seat for a short time without falsely selling it.
QHow would this work with multiple application instances?
Replace the in-memory synchronized lock provider with database row locks, an atomic update on show-seat rows, or a distributed lock with TTL.
QWhere do notifications and seat-map refreshes belong?
They are observers of Show seat changes. The domain emits availability events; WebSocket, email, and analytics adapters subscribe without polluting booking logic.
Production Considerations
Durable holds
Store holds with expiry timestamps so a restart does not forget blocked seats. A scheduled job or database TTL can release expired holds.
Payment authorization
Prefer authorize then capture. Capture after the seat becomes BOOKED; if confirmation fails, void the authorization instead of refunding captured money.
Idempotency
Make confirm and payment callbacks idempotent by using booking id and payment transaction id. Duplicate callbacks must not double-confirm or double-charge.
Observability
Track hold success rate, hold expiry rate, payment failure rate, lock contention, and seat-map latency for each show.
Fairness and abuse prevention
Limit seats per customer, throttle repeated holds, and shorten hold duration for high-demand shows to prevent seat hoarding.
What Interviewers Look For
Did the candidate identify show-specific seats, not just screen seats?
Did they explicitly guard the check-then-hold race for the same seat?
Is the booking state machine legal and testable?
Are pricing and allocation policies behind Strategy interfaces?
Can observers refresh availability without coupling UI code to booking logic?
Can the in-memory lock be swapped for a database or distributed lock in production?
Quiz
0/5 answered
1.Why is the HELD state necessary before CONFIRMED?
2.What is the critical concurrency section in this design?
3.Which pattern best describes **BookingState**?
4.Why is **PricingStrategy** separated from **BookingService**?
5.What should happen if a payment fails after seats are held?
Practice Variants
Add row-aware contiguous allocation
IntermediateRepresent row and seat number separately, then allocate K adjacent seats in the same row before falling back to nearby seats.
Support coupons and surge pricing
IntermediateCompose pricing strategies for base seat type price, weekend multiplier, coupon discount, and convenience fee.
Replace in-memory locks with database locks
AdvancedModel show-seat rows with status and version, then implement hold using an atomic update or row-level transaction.
Add waitlist notifications
AdvancedWhen a hold expires or a booking is cancelled, notify waitlisted customers through an observer-driven notification adapter.
Flashcards
Cheat Sheet
Core model: Cinema → Screen → Show → Seat. Availability is show-specific, not global to the physical screen.
Booking lifecycle: Created → SeatsHeld → Confirmed, Expired, or Cancelled. Confirmed is terminal in the base design.
Concurrency invariant: one show-seat can have at most one active hold or confirmed booking. Guard check-then-hold with a synchronized lock provider or transaction.
Patterns: State for booking transitions, Strategy for pricing and allocation, Observer for seat availability, Singleton for the in-memory controller.
Payment rule: authorize or charge only while the hold is valid; mark seats BOOKED before returning a confirmed ticket.
Scale path: persist show seats and holds, add idempotent payment callbacks, and use row locks or atomic status updates for multi-instance safety.
References
- BookJava Concurrency in Practice — Brian Goetz et al.
- DocsRefactoring Guru — State Pattern
- DocsRefactoring Guru — Strategy Pattern
- BlogMartin Fowler — Unit of Work — Martin Fowler