Episode 9 — System Design / 9.6 — LLD Problem Solving
9.6.c — BookMyShow (Movie Booking System)
Problem: Design a movie ticket booking platform where users browse movies, select a show, pick seats, and make a booking. The system must handle concurrent seat selection without double-booking.
Table of Contents
- Requirements
- Entities and Enums
- Class Diagram
- Sequence Flows
- Complete Implementation
- Usage Walkthrough
- Edge Cases
- Key Takeaways
Requirements
Functional Requirements
- The system manages multiple theatres in multiple cities.
- Each theatre has multiple screens.
- Each screen has a fixed layout of seats (rows and columns).
- Movies are scheduled as shows on specific screens at specific times.
- Users can browse shows for a movie, select seats, and book them.
- Selected seats are temporarily locked (5 min) to prevent double-booking.
- If payment succeeds, the booking is confirmed; otherwise, the lock is released.
- Users can cancel a confirmed booking.
Non-Functional Requirements
- Concurrent seat selection must be safe (no two users can book the same seat).
- Extensible seat pricing (regular, premium, VIP).
Out of Scope
- Payment gateway integration (simulated).
- Recommendations, reviews, ratings.
- Food ordering.
Entities and Enums
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ SeatType │ │ SeatStatus │ │ BookingStatus │
│────────────────────│ │────────────────────│ │────────────────────│
│ REGULAR │ │ AVAILABLE │ │ PENDING │
│ PREMIUM │ │ LOCKED │ │ CONFIRMED │
│ VIP │ │ BOOKED │ │ CANCELLED │
└────────────────────┘ └────────────────────┘ └────────────────────┘
┌────────────────────┐
│ PaymentStatus │
│────────────────────│
│ PENDING │
│ SUCCESS │
│ FAILED │
└────────────────────┘
Class Diagram
┌──────────────────────────────────┐
│ Movie │
│──────────────────────────────────│
│ - id: string │
│ - title: string │
│ - durationMin: number │
│ - genre: string │
│ - language: string │
└──────────────┬───────────────────┘
│ shown in
▼
┌──────────────────────────────────┐ ┌──────────────────────────────┐
│ Show │ │ Theatre │
│──────────────────────────────────│ │──────────────────────────────│
│ - id: string │◄──────│ - id: string │
│ - movie: Movie │ │ - name: string │
│ - screen: Screen │ │ - city: string │
│ - startTime: Date │ │ - screens: Screen[] │
│ - endTime: Date │ │──────────────────────────────│
│ - seatPricing: Map<SeatType,num> │ │ + addScreen(screen): void │
│ - seatStatus: Map<seatId, status>│ └──────────────┬───────────────┘
│──────────────────────────────────│ │ has-many
│ + getAvailableSeats(): Seat[] │ ▼
│ + lockSeats(seatIds, userId): bool│ ┌──────────────────────────────┐
│ + bookSeats(seatIds): void │ │ Screen │
│ + releaseSeats(seatIds): void │ │──────────────────────────────│
└──────────────────────────────────┘ │ - id: string │
│ - name: string │
│ - seats: Seat[] │
│──────────────────────────────│
│ + getSeatMap(): Seat[][] │
└──────────────┬───────────────┘
│ has-many
▼
┌──────────────────────────────────┐ ┌──────────────────────────────┐
│ Booking │ │ Seat │
│──────────────────────────────────│ │──────────────────────────────│
│ - id: string │ │ - id: string │
│ - user: User │ │ - row: string │
│ - show: Show │ │ - number: number │
│ - seats: Seat[] │ │ - type: SeatType │
│ - totalPrice: number │ └──────────────────────────────┘
│ - status: BookingStatus │
│ - createdAt: Date │
│──────────────────────────────────│ ┌──────────────────────────────┐
│ + confirm(): void │ │ User │
│ + cancel(): void │ │──────────────────────────────│
└──────────────────────────────────┘ │ - id: string │
│ - name: string │
┌──────────────────────────────────┐ │ - email: string │
│ BookingService │ └──────────────────────────────┘
│──────────────────────────────────│
│ - bookings: Map<id, Booking> │
│ - lockTimeoutMs: number │
│──────────────────────────────────│
│ + startBooking(user, show, seats) │
│ + confirmBooking(bookingId): bool │
│ + cancelBooking(bookingId): void │
│ + cleanExpiredLocks(): void │
└──────────────────────────────────┘
Sequence Flows
Seat Selection and Booking Flow
User BookingService Show Seat Payment
│ │ │ │ │
│── browse ────►│ │ │ │
│◄── shows[] ───│ │ │ │
│ │ │ │ │
│── getSeats() ►│── available? ──►│ │ │
│◄── seatMap ───│◄── seats[] ────│ │ │
│ │ │ │ │
│── startBooking(show, [A1,A2])──►│ │ │
│ │── lockSeats() ─►│ │ │
│ │ │── lock A1,A2 ─►│ │
│ │◄── locked ──────│◄── ok ────────│ │
│◄── booking │ │ │ │
│ (PENDING) │ │ │ │
│ │ │ │ │
│── confirmBooking(id) ──────────►│ │ │
│ │── processPayment() ────────────────────────────►│
│ │◄── SUCCESS ────────────────────────────────────│
│ │── bookSeats() ─►│ │ │
│ │ │── book A1,A2 ─►│ │
│◄── CONFIRMED │◄── done ───────│◄── ok ────────│ │
Lock Expiry Flow
Timer BookingService Show
│ │ │
│── tick (5 min) ─►│ │
│ │── cleanExpiredLocks()
│ │── find expired ─►│
│ │ │── release seats
│ │── cancel booking │
│ │◄── done ────────│
Complete Implementation
Enums
// ─── Enums ──────────────────────────────────────────────
const SeatType = Object.freeze({
REGULAR: 'REGULAR',
PREMIUM: 'PREMIUM',
VIP: 'VIP',
});
const SeatStatus = Object.freeze({
AVAILABLE: 'AVAILABLE',
LOCKED: 'LOCKED',
BOOKED: 'BOOKED',
});
const BookingStatus = Object.freeze({
PENDING: 'PENDING',
CONFIRMED: 'CONFIRMED',
CANCELLED: 'CANCELLED',
});
const PaymentStatus = Object.freeze({
PENDING: 'PENDING',
SUCCESS: 'SUCCESS',
FAILED: 'FAILED',
});
Core Entities
// ─── User ───────────────────────────────────────────────
class User {
static _counter = 0;
constructor(name, email) {
this.id = `U-${++User._counter}`;
this.name = name;
this.email = email;
}
}
// ─── Movie ──────────────────────────────────────────────
class Movie {
static _counter = 0;
constructor(title, durationMin, genre, language) {
this.id = `MOV-${++Movie._counter}`;
this.title = title;
this.durationMin = durationMin;
this.genre = genre;
this.language = language;
}
}
// ─── Seat ───────────────────────────────────────────────
class Seat {
constructor(row, number, type = SeatType.REGULAR) {
this.id = `${row}${number}`;
this.row = row;
this.number = number;
this.type = type;
}
}
Screen
// ─── Screen ─────────────────────────────────────────────
class Screen {
static _counter = 0;
constructor(name, seatLayout) {
this.id = `SCR-${++Screen._counter}`;
this.name = name;
this.seats = [];
// seatLayout example:
// [
// { row: 'A', count: 10, type: SeatType.VIP },
// { row: 'B', count: 10, type: SeatType.PREMIUM },
// { row: 'C', count: 12, type: SeatType.REGULAR },
// { row: 'D', count: 12, type: SeatType.REGULAR },
// ]
for (const config of seatLayout) {
for (let i = 1; i <= config.count; i++) {
this.seats.push(new Seat(config.row, i, config.type));
}
}
}
getSeatById(seatId) {
return this.seats.find(s => s.id === seatId) || null;
}
getSeatMap() {
const map = {};
for (const seat of this.seats) {
if (!map[seat.row]) map[seat.row] = [];
map[seat.row].push(seat);
}
return map;
}
}
Theatre
// ─── Theatre ────────────────────────────────────────────
class Theatre {
static _counter = 0;
constructor(name, city) {
this.id = `TH-${++Theatre._counter}`;
this.name = name;
this.city = city;
this.screens = [];
}
addScreen(screen) {
this.screens.push(screen);
}
}
Show
// ─── Show ───────────────────────────────────────────────
// A show is a specific screening of a movie on a screen at a time.
// It tracks seat status independently (same screen can have different
// shows with different seat availability).
class Show {
static _counter = 0;
constructor(movie, screen, startTime, seatPricing) {
this.id = `SHOW-${++Show._counter}`;
this.movie = movie;
this.screen = screen;
this.startTime = startTime;
this.endTime = new Date(startTime.getTime() + movie.durationMin * 60000);
// seatPricing: { REGULAR: 200, PREMIUM: 350, VIP: 500 }
this.seatPricing = seatPricing;
// Track status per seat for THIS show
// seatId -> { status, lockedBy, lockedAt }
this.seatStatus = new Map();
for (const seat of screen.seats) {
this.seatStatus.set(seat.id, {
status: SeatStatus.AVAILABLE,
lockedBy: null,
lockedAt: null,
});
}
}
getAvailableSeats() {
const available = [];
for (const seat of this.screen.seats) {
const info = this.seatStatus.get(seat.id);
if (info.status === SeatStatus.AVAILABLE) {
available.push({
...seat,
price: this.seatPricing[seat.type] || 0,
});
}
}
return available;
}
getSeatPrice(seatId) {
const seat = this.screen.getSeatById(seatId);
return seat ? (this.seatPricing[seat.type] || 0) : 0;
}
// Lock seats for a user (temporary hold)
lockSeats(seatIds, userId) {
// First verify all seats are available
for (const seatId of seatIds) {
const info = this.seatStatus.get(seatId);
if (!info || info.status !== SeatStatus.AVAILABLE) {
return false; // At least one seat not available
}
}
// All available — lock them atomically
const now = new Date();
for (const seatId of seatIds) {
this.seatStatus.set(seatId, {
status: SeatStatus.LOCKED,
lockedBy: userId,
lockedAt: now,
});
}
return true;
}
// Confirm booking — move from LOCKED to BOOKED
bookSeats(seatIds) {
for (const seatId of seatIds) {
const info = this.seatStatus.get(seatId);
if (info && info.status === SeatStatus.LOCKED) {
this.seatStatus.set(seatId, {
status: SeatStatus.BOOKED,
lockedBy: null,
lockedAt: null,
});
}
}
}
// Release seats (cancel or lock expiry)
releaseSeats(seatIds) {
for (const seatId of seatIds) {
this.seatStatus.set(seatId, {
status: SeatStatus.AVAILABLE,
lockedBy: null,
lockedAt: null,
});
}
}
// Find all expired locks
getExpiredLocks(timeoutMs) {
const now = Date.now();
const expired = [];
for (const [seatId, info] of this.seatStatus) {
if (
info.status === SeatStatus.LOCKED &&
info.lockedAt &&
now - info.lockedAt.getTime() > timeoutMs
) {
expired.push({ seatId, userId: info.lockedBy });
}
}
return expired;
}
}
Booking
// ─── Booking ────────────────────────────────────────────
class Booking {
static _counter = 0;
constructor(user, show, seatIds) {
this.id = `BK-${++Booking._counter}`;
this.user = user;
this.show = show;
this.seatIds = seatIds;
this.totalPrice = seatIds.reduce((sum, id) => sum + show.getSeatPrice(id), 0);
this.status = BookingStatus.PENDING;
this.createdAt = new Date();
}
confirm() {
this.status = BookingStatus.CONFIRMED;
}
cancel() {
this.status = BookingStatus.CANCELLED;
}
}
BookingService (Controller)
// ─── BookingService ─────────────────────────────────────
class BookingService {
constructor(lockTimeoutMs = 5 * 60 * 1000) {
this.bookings = new Map(); // bookingId -> Booking
this.lockTimeoutMs = lockTimeoutMs;
}
// Step 1: User selects seats. Lock them and create a PENDING booking.
startBooking(user, show, seatIds) {
// Attempt to lock seats
const locked = show.lockSeats(seatIds, user.id);
if (!locked) {
throw new Error('One or more seats are not available. Please select different seats.');
}
const booking = new Booking(user, show, seatIds);
this.bookings.set(booking.id, booking);
console.log(
`[BOOKING] ${booking.id} created for ${user.name}. ` +
`Seats: ${seatIds.join(', ')}. Total: $${booking.totalPrice}. ` +
`Status: PENDING (locked for ${this.lockTimeoutMs / 60000} min)`
);
return booking;
}
// Step 2: User pays. Confirm the booking.
confirmBooking(bookingId) {
const booking = this.bookings.get(bookingId);
if (!booking) throw new Error(`Booking ${bookingId} not found.`);
if (booking.status !== BookingStatus.PENDING) {
throw new Error(`Booking ${bookingId} is ${booking.status}, cannot confirm.`);
}
// Simulate payment
const paymentSuccess = this._processPayment(booking);
if (paymentSuccess) {
booking.show.bookSeats(booking.seatIds);
booking.confirm();
console.log(`[CONFIRMED] ${booking.id} for ${booking.user.name}.`);
return true;
} else {
// Payment failed — release seats
booking.show.releaseSeats(booking.seatIds);
booking.cancel();
console.log(`[PAYMENT FAILED] ${booking.id}. Seats released.`);
return false;
}
}
// Cancel a confirmed or pending booking
cancelBooking(bookingId) {
const booking = this.bookings.get(bookingId);
if (!booking) throw new Error(`Booking ${bookingId} not found.`);
if (booking.status === BookingStatus.CANCELLED) {
throw new Error('Booking is already cancelled.');
}
booking.show.releaseSeats(booking.seatIds);
booking.cancel();
console.log(`[CANCELLED] ${booking.id}. Seats released. Refund: $${booking.totalPrice}`);
}
// Clean up expired locks (call periodically)
cleanExpiredLocks(shows) {
for (const show of shows) {
const expired = show.getExpiredLocks(this.lockTimeoutMs);
if (expired.length > 0) {
const seatIds = expired.map(e => e.seatId);
show.releaseSeats(seatIds);
console.log(`[CLEANUP] Released expired locks: ${seatIds.join(', ')} for show ${show.id}`);
}
}
// Cancel pending bookings whose seats were released
for (const [id, booking] of this.bookings) {
if (booking.status === BookingStatus.PENDING) {
const elapsed = Date.now() - booking.createdAt.getTime();
if (elapsed > this.lockTimeoutMs) {
booking.cancel();
console.log(`[CLEANUP] Cancelled expired booking ${id}`);
}
}
}
}
// Simulated payment
_processPayment(booking) {
// In real system: call payment gateway
console.log(`[PAYMENT] Processing $${booking.totalPrice} for ${booking.user.name}...`);
return true; // Always succeeds in simulation
}
}
Usage Walkthrough
// ─── Demo ───────────────────────────────────────────────
// Setup: Theatre with a screen
const screen1 = new Screen('Audi 1', [
{ row: 'A', count: 8, type: SeatType.VIP },
{ row: 'B', count: 10, type: SeatType.PREMIUM },
{ row: 'C', count: 12, type: SeatType.REGULAR },
{ row: 'D', count: 12, type: SeatType.REGULAR },
]);
const theatre = new Theatre('PVR Cinemas', 'Mumbai');
theatre.addScreen(screen1);
// Schedule a movie
const movie = new Movie('Inception', 148, 'Sci-Fi', 'English');
const show = new Show(movie, screen1, new Date('2026-04-12T18:00:00'), {
[SeatType.REGULAR]: 200,
[SeatType.PREMIUM]: 350,
[SeatType.VIP]: 500,
});
// Booking service
const service = new BookingService(5 * 60 * 1000); // 5 min lock
// User 1 browses and books
const user1 = new User('Alice', 'alice@example.com');
console.log('Available seats:', show.getAvailableSeats().length); // 42
const booking1 = service.startBooking(user1, show, ['A1', 'A2']);
// [BOOKING] BK-1 created for Alice. Seats: A1, A2. Total: $1000. Status: PENDING
service.confirmBooking(booking1.id);
// [PAYMENT] Processing $1000 for Alice...
// [CONFIRMED] BK-1 for Alice.
// User 2 tries the same seats
const user2 = new User('Bob', 'bob@example.com');
try {
service.startBooking(user2, show, ['A1', 'A3']);
// A1 is BOOKED, so this fails
} catch (err) {
console.log(err.message);
// "One or more seats are not available. Please select different seats."
}
// User 2 picks different seats
const booking2 = service.startBooking(user2, show, ['A3', 'A4']);
service.confirmBooking(booking2.id);
// User 1 cancels
service.cancelBooking(booking1.id);
// [CANCELLED] BK-1. Seats released. Refund: $1000
// Now A1, A2 are available again
console.log('A1 status:', show.seatStatus.get('A1').status); // AVAILABLE
Edge Cases
| Edge Case | How It Is Handled |
|---|---|
| Two users select the same seat simultaneously | lockSeats() checks all seats first; only the first caller succeeds |
| Lock expires before payment | cleanExpiredLocks() releases seats and cancels booking |
| Payment fails after lock | confirmBooking() releases seats and marks booking CANCELLED |
| User cancels a confirmed booking | cancelBooking() releases seats and marks CANCELLED |
| User tries to book 0 seats | startBooking with empty array creates a $0 booking (add validation) |
| Show time has passed | Add time validation in startBooking (not shown for brevity) |
| Same user books same seat twice | Seat is already BOOKED/LOCKED, lockSeats() returns false |
Concurrency Note
In a real system (multi-threaded / distributed), lockSeats() would use:
- Database-level row locking (
SELECT ... FOR UPDATE) - Optimistic concurrency (version numbers on seat rows)
- Redis distributed lock for the seat selection window
In JavaScript (single-threaded), the sequential check-then-lock in lockSeats() is sufficient.
Key Takeaways
- Seat locking is the critical design decision — it prevents double-booking without requiring pessimistic locks on the entire show.
- The Show entity owns seat status (not the Screen), because the same screen can have different shows with independent availability.
- BookingService acts as the orchestrator — it coordinates between Show, Booking, and Payment.
- Temporal lock expiry is essential: without it, abandoned carts would permanently hold seats.
- In interviews, always discuss the concurrency strategy even if your implementation is single-threaded — it shows you understand real-world constraints.
Next -> 9.6.d — Splitwise