LLD for Movie Booking System: Designing BookMyShow
The Cinema Inventory Problem: What Makes It Hard
A Friday night show has 200 seats.
🎯 System Scenario & Requirements
System Goal
A Friday night show has 200 seats.
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- A Friday night show has 200 seats.
- Two scenarios break a naive implementation: 1.
- Double-booking: User A and User B click "Book Seat A1" at the same millisecond.
Constraints & Assumptions
- Failure mode — skipping the LOCKED state: If code transitions directly from AVAILABLE to BOOKED in a single step, any failure mid-payment (timeout, gateway error, network drop) leaves the seat permanently BOOKED with no…
- The LOCKED intermediate state isolates the inventory hold from the payment outcome, making reversal explicit, TTL-bounded, and safe.
⚡ Execution Stages & Decision Trace
Stage 1 — Key Entities in a Movie Booking System
Before writing a single line of concurrency code, you need a clear domain model.
A movie booking platform has six core entities: SeatType classifies seats by price tier: - PLATINUM — premium recliner seats (front/center) - GOLD — standard premium tier - SILVER — economy tier SeatStatus is the concur…
Deep Dive
Scenario context
You run a cinema. A Friday night show has 200 seats. Two scenarios break a naive implementation:
- Double-booking: User A and User B click "Book Seat A1" at the same millisecond. Only one should succeed.
- Abandoned locks: User A locks Seat A1 but never completes payment. The seat should become available again after 10 minutes.
This needs a 3-state model + distributed lock strategy.
This walkthrough focuses on The Cinema Inventory Problem: What Makes It Hard.
Before making a design choice, clarify:
- A Friday night show has 200 seats.
- Two scenarios break a naive implementation: 1.
- Double-booking: User A and User B click "Book Seat A1" at the same millisecond.
Concepts in play: low-level-design-systems, caching, api-gateway, distributed-locking
Stage 1 — Key Entities in a Movie Booking System
- Before writing a single line of concurrency code, you need a clear domain model.
- A movie booking platform has six core entities: SeatType classifies seats by price tier: - PLATINUM — premium recliner seats (front/center) - GOLD — standard premium tier - SILVER — economy tier SeatStatus is the concur…
- Every Booking references a Show, not the Movie directly — this is why "the 7 PM showing of a film" is completely separate inventory from "the 9 PM showing" of the same film.
- This hierarchy shapes every query, index, and cache partition in the real system.
Observable outcome: This hierarchy shapes every query, index, and cache partition in the real system.
Stage 2 — The Domain Hierarchy: City → Cinema → Screen → Show → Seat
- The composition arrows ( -- ) running down the left spine — City → Cinema → Screen → Seat — model physical containment: a Screen cannot exist without its owning Cinema , and Seat instances are part of the Screen .
- Show sits at the centre as the critical join entity, linking a Movie and a Screen at a specific startTime ; every Booking references a Show , not a Movie directly, which is why two screenings of the same film are comple…
- The aggregation arrows ( o-- ) from BookingService to PricingStrategy and PaymentGateway express collaboration, not ownership — both dependencies are injected interfaces, making the service fully decoupled from any conc…
- SeatType: GOLD , SILVER , PLATINUM — affects pricing.
classDiagram
class City {+String name; +List~Cinema~ cinemas}
class Cinema {+String name; +List~Screen~ screens}
class Screen {+int id; +List~Seat~ seats}
class Movie {+String title; +int durationMinutes}
class Show {
+Movie movie
+Screen screen
+LocalDateTime startTime
+synchronized boolean bookSeats(List seats)
}
class Seat {
-SeatStatus status
+String row
+int col
+SeatType type
+reserve(bookingId)
+release()
+confirm()
}
class PremiumSeat {
+boolean hasRecline
+boolean hasFootrest
}
class GroupSeat {
+String groupReservationId
}
class Booking {
+int id
+Show show
+List~Seat~ seats
+PaymentStatus payment
}
class BookingService {
+reserveSeats(User, Show, List~Seat~)
+confirmBooking(Booking)
}
class PricingStrategy {
<>
+calculatePrice(Seat, Show, Customer) Money
}
class PaymentGateway {
<>
+charge(Customer, Money, String) PaymentResult
}
City *-- Cinema
Cinema *-- Screen
Screen *-- Seat
Show "1" *-- "many" Seat : contains
Show --> Screen
Booking --> Show
Booking --> Seat
PremiumSeat --|> Seat : extends
GroupSeat --|> Seat : extends
BookingService o-- PricingStrategy : uses (injected)
BookingService o-- PaymentGateway : uses (injected)
Observable outcome: - SeatType: GOLD , SILVER , PLATINUM — affects pricing.
Stage 3 — The 3-State Seat Model: Preventing Double Booking
- The key insight: don't go directly from AVAILABLE to BOOKED.
- Insert a temporary LOCKED state.
- State machine: The booking flow: User B gets a graceful "This seat is currently being held by another user" message — not a race condition with undefined behavior.
stateDiagram-v2
[*] --> AVAILABLE
AVAILABLE --> LOCKED : User selects seat (10 min TTL)
LOCKED --> BOOKED : Payment success
LOCKED --> AVAILABLE : Payment timeout / failure
BOOKED --> [*] : Show complete
Observable outcome: State machine: The booking flow: User B gets a graceful "This seat is currently being held by another user" message — not a race condition with undefined behavior.
Stage 4 — Booking Flow: From Seat Selection to Confirmation
- Here is how a booking request moves through the system end-to-end — including both failure paths that keep inventory clean: The flowchart maps the complete booking transaction as a directed decision graph with two disti…
- The first diamond — "All seats AVAILABLE?" — is the cheap early-exit: if any seat is already LOCKED or BOOKED by a concurrent user, the request is rejected before a lock is ever acquired on the current user's behalf, re…
- Only after seats are successfully locked does control reach the payment gateway, whose failure branch explicitly releases the hold and returns inventory to AVAILABLE — this explicit release is what separates a well-desi…
- Two distinct failure paths keep inventory accurate: 1.
flowchart TD
A([User Selects Seats]) --> B{All seats AVAILABLE?}
B -- No --> C([Error: Seat Already Held by Another User])
B -- Yes --> D[Lock Seats 10 min TTL applied]
D --> E([User Redirected to Payment Page])
E --> F{Payment Successful?}
F -- Timeout or Failure --> G[Release Lock Seats set back to AVAILABLE]
F -- Yes --> H[Set Seats to BOOKED]
H --> I([Confirmation Email Sent to User])
G --> J([Error: Payment Failed Seats Released])
Observable outcome: Two distinct failure paths keep inventory accurate: 1.
Stage 5 — Deep Dive: Synchronized Booking Implementation
- synchronized on the Show method ensures that the check-and-lock is atomic — no two threads can interleave between the check ( AVAILABLE ) and the lock ( LOCKED ).
- 📊 Reserve Seats: Booking Sequence The sequence diagram traces the critical path for seat reservation — from the initial lock request (preventing other threads from booking the same seat) through payment confirmation to…
- The synchronized annotation on the BS- SH call marks the boundary of the atomic monitor: every arrow that follows — the status check ( SH- S: status == AVAILABLE?
- ) and the lock ( SH- S: reserve(bookingId) ) — executes inside a single thread-held critical section, making interleaving between the check and the commit impossible.
public class Show {
private final int id;
private final List<Seat> seats;
public synchronized BookingResult reserveSeats(User user, List<Integer> seatIds) {
// Phase 1: Check all requested seats are available
List<Seat> selected = new ArrayList<>();
for (int id : seatIds) {
Seat s = findSeat(id);
if (s.getStatus() != SeatStatus.AVAILABLE) {
return BookingResult.failure("Seat " + id + " is not available");
}
selected.add(s);
}
// Phase 2: Lock all seats together (atomic within synchronized block)
Instant lockExpiry = Instant.now().plusSeconds(600);
for (Seat s : selected) {
s.setStatus(SeatStatus.LOCKED);
s.setLockExpiry(lockExpiry);
}
return BookingResult.success(new Booking(user, this, selected));
}
}
sequenceDiagram
participant U as User
participant BS as BookingService
participant SH as Show
participant S as Seat
participant PG as PaymentGateway
U->>BS: reserveSeats(user, show, seatIds)
BS->>SH: reserveSeats(user, seatIds) synchronized
SH->>S: status == AVAILABLE?
S-->>SH: true
SH->>S: reserve(bookingId) LOCKED
SH-->>BS: BookingResult.success(booking)
BS->>PG: charge(user, amount, bookingId)
PG-->>BS: PaymentResult.success
BS->>S: confirm() BOOKED
BS-->>U: Booking{id, seats, CONFIRMED}
Observable outcome: ) and the lock ( SH- S: reserve(bookingId) ) — executes inside a single thread-held critical section, making interleaving between the check and the commit impossible.
Stage 6 — Pricing Strategy Pattern
- Different shows and seat types need different prices: 📊 Enhanced Booking Domain Class Diagram This enhanced diagram isolates the Seat subsystem with its full type hierarchy and wires in the three Strategy/Gateway inter…
- The enumeration classes SeatType and SeatStatus appear as owned associations of Seat (solid arrows from Seat ), making explicit that price tier and booking state are intrinsic to the seat itself — not to the Booking or…
- The dashed implementation arrows ( ..
- ) from HourlyPricing and DynamicPricing to PricingStrategy are the Strategy pattern in UML form: both are interchangeable at the BookingService injection site, so the active pricing rule switches at Spring configuration…
public interface PricingStrategy {
Money calculatePrice(Seat seat, Show show, Customer customer);
}
public class HourlyPricing implements PricingStrategy {
public Money calculatePrice(Seat seat, Show show, Customer customer) {
double base = seat.getType() == SeatType.GOLD ? 15.0 : 10.0;
double multiplier = show.isWeekend() ? 1.2 : 1.0; // weekend surcharge
return Money.of(base * multiplier);
}
}
public class DynamicPricing implements PricingStrategy {
public Money calculatePrice(Seat seat, Show show, Customer customer) {
double occupancy = show.getBookedPercent();
return Money.of(BASE_PRICE * (1 + occupancy)); // price rises with demand
}
}
classDiagram
class Seat {
-SeatStatus status
+String row
+int col
+SeatType type
+reserve(String bookingId) void
+release() void
+confirm() void
}
class PremiumSeat {
+boolean hasRecline
+boolean hasFootrest
}
class GroupSeat {
+String groupReservationId
}
class SeatType {
<>
PLATINUM
GOLD
SILVER
}
class SeatStatus {
<>
AVAILABLE
LOCKED
BOOKED
}
class PricingStrategy {
<>
+calculatePrice(Seat, Show, Customer) Money
}
class HourlyPricing {
+calculatePrice(Seat, Show, Customer) Money
}
class DynamicPricing {
+calculatePrice(Seat, Show, Customer) Money
}
class SeatSelector {
<>
+findAvailable(Show, SeatType, int) List~Seat~
}
class PaymentGateway {
<>
+charge(Customer, Money, String) PaymentResult
}
class BookingService {
-PricingStrategy pricer
-SeatSelector selector
-PaymentGateway gateway
+reserveSeats(User, Show, List~Seat~) Booking
+confirmBooking(Booking) void
}
PremiumSeat --|> Seat : extends
GroupSeat --|> Seat : extends
Seat --> SeatStatus : state
Seat --> SeatType : type
HourlyPricing ..|> PricingStrategy : implements
DynamicPricing ..|> PricingStrategy : implements
BookingService o-- PricingStrategy : uses (injected)
BookingService o-- SeatSelector : uses (injected)
BookingService o-- PaymentGateway : uses (injected)
Observable outcome: ) from HourlyPricing and DynamicPricing to PricingStrategy are the Strategy pattern in UML form: both are interchangeable at the BookingService injection site, so the active pricing rule switches at Spring configuration…
End-to-end outcome
A Friday night show has 200 seats.
Alternatives considered
OOP Decision Guide: Patterns and Extension Points
Start with synchronized on Seat methods and constructor-injected strategies.
Failure and degraded modes
OOP Design Trade-offs and Failure Modes in the Booking System
- Detection: Failure mode — skipping the LOCKED state: If code transitions directly from AVAILABLE to BOOKED in a single step, any failure mid-payment (timeout, gateway error, network drop) leaves the seat perman…
- User or operator impact: Failure mode — skipping the LOCKED state: If code transitions directly from AVAILABLE to BOOKED in a single step, any failure mid-payment (timeout, gateway error, network drop) leaves the seat perman…
