BookMyShow (or any movie ticket booking system) is a top-tier LLD question. What makes it interesting isn’t the movie/theater modeling — that part is straightforward. The real challenge is seat locking. Two people looking at the same show, same seats, at the same time. Who gets them? That concurrency problem is what separates a good answer from a great one.
Let’s design it from the ground up.
Requirements
Functional:
- Browse movies playing in a city
- View theaters and shows for a movie
- Select seats from an available seat map
- Book selected seats (with temporary lock while paying)
- Process payment and confirm booking
- Cancel booking
The Big Constraint:
- Two users CANNOT book the same seat. When User A selects seats, those seats are temporarily locked for 5 minutes. If User A doesn’t pay in time, the lock expires and the seats go back to available.
Key Classes & Relationships
seat_status: Map<Seat, SeatStatus>
lock_time, user
REGULAR / PREMIUM / VIP
status, timestamp
Core Implementation
from enum import Enum
from datetime import datetime, timedelta
from threading import Lock
import uuid
# ---- Enums ----
class SeatType(Enum):
REGULAR = "REGULAR"
PREMIUM = "PREMIUM"
VIP = "VIP"
class SeatStatus(Enum):
AVAILABLE = "AVAILABLE"
LOCKED = "LOCKED" # temporarily held during payment
BOOKED = "BOOKED"
class BookingStatus(Enum):
PENDING = "PENDING" # seats locked, waiting for payment
CONFIRMED = "CONFIRMED"
CANCELLED = "CANCELLED"
EXPIRED = "EXPIRED" # lock timed out
# ---- Core Models ----
class Movie:
def __init__(self, title: str, duration_mins: int, genre: str):
self.movie_id = str(uuid.uuid4())[:8]
self.title = title
self.duration_mins = duration_mins
self.genre = genre
class Seat:
def __init__(self, row: str, number: int, seat_type: SeatType):
self.seat_id = f"{row}{number}"
self.row = row
self.number = number
self.seat_type = seat_type
SEAT_PRICES = {
SeatType.REGULAR: 200,
SeatType.PREMIUM: 350,
SeatType.VIP: 500,
}
class Screen:
def __init__(self, screen_number: int, seats: list):
self.screen_number = screen_number
self.seats = seats # list of Seat
class Theater:
def __init__(self, name: str, city: str, screens: list):
self.name = name
self.city = city
self.screens = screens # list of Screen
# ---- Show: the heart of the system ----
class Show:
LOCK_DURATION = timedelta(minutes=5)
def __init__(self, movie: Movie, screen: Screen, start_time: datetime):
self.show_id = str(uuid.uuid4())[:8]
self.movie = movie
self.screen = screen
self.start_time = start_time
# Per-show seat status tracking
self.seat_status = {seat.seat_id: SeatStatus.AVAILABLE for seat in screen.seats}
self.seat_locks = {} # seat_id -> lock_expiry time
self._lock = Lock() # thread safety!
def get_available_seats(self) -> list:
self._expire_locks()
return [sid for sid, status in self.seat_status.items()
if status == SeatStatus.AVAILABLE]
def lock_seats(self, seat_ids: list) -> bool:
"""Try to lock seats for a user. Returns True if ALL seats locked."""
with self._lock: # Thread-safe!
self._expire_locks()
# Check all seats are available FIRST
for sid in seat_ids:
if self.seat_status.get(sid) != SeatStatus.AVAILABLE:
return False
# Lock them all
expiry = datetime.now() + self.LOCK_DURATION
for sid in seat_ids:
self.seat_status[sid] = SeatStatus.LOCKED
self.seat_locks[sid] = expiry
return True
def confirm_seats(self, seat_ids: list):
"""Convert locked seats to booked."""
with self._lock:
for sid in seat_ids:
self.seat_status[sid] = SeatStatus.BOOKED
self.seat_locks.pop(sid, None)
def release_seats(self, seat_ids: list):
"""Release locked/booked seats back to available."""
with self._lock:
for sid in seat_ids:
self.seat_status[sid] = SeatStatus.AVAILABLE
self.seat_locks.pop(sid, None)
def _expire_locks(self):
"""Release any seats whose lock has expired."""
now = datetime.now()
expired = [sid for sid, expiry in self.seat_locks.items() if now > expiry]
for sid in expired:
self.seat_status[sid] = SeatStatus.AVAILABLE
del self.seat_locks[sid]
# ---- Booking ----
class Booking:
def __init__(self, user_id: str, show: Show, seat_ids: list):
self.booking_id = str(uuid.uuid4())[:8]
self.user_id = user_id
self.show = show
self.seat_ids = seat_ids
self.status = BookingStatus.PENDING
self.created_at = datetime.now()
self.amount = self._calculate_amount()
def _calculate_amount(self) -> float:
total = 0
seat_map = {s.seat_id: s for s in self.show.screen.seats}
for sid in self.seat_ids:
seat = seat_map[sid]
total += SEAT_PRICES[seat.seat_type]
return total
# ---- Payment ----
class Payment:
def __init__(self, booking: Booking, method: str):
self.payment_id = str(uuid.uuid4())[:8]
self.booking = booking
self.amount = booking.amount
self.method = method
self.paid_at = datetime.now()
# ---- Booking Service (ties it all together) ----
class BookingService:
def __init__(self):
self.bookings = {} # booking_id -> Booking
def create_booking(self, user_id: str, show: Show, seat_ids: list) -> Booking:
"""Step 1: Lock seats and create pending booking."""
if not show.lock_seats(seat_ids):
print(f"Seats {seat_ids} not available -- someone beat us to it!")
return None
booking = Booking(user_id, show, seat_ids)
self.bookings[booking.booking_id] = booking
print(f"Booking {booking.booking_id} created. Amount: ${booking.amount}")
print(f"Seats locked for 5 minutes. Complete payment to confirm.")
return booking
def confirm_booking(self, booking_id: str, payment_method: str) -> Payment:
"""Step 2: Pay and confirm the booking."""
booking = self.bookings.get(booking_id)
if not booking or booking.status != BookingStatus.PENDING:
print("Invalid booking!")
return None
# Check if lock expired
elapsed = datetime.now() - booking.created_at
if elapsed > Show.LOCK_DURATION:
booking.status = BookingStatus.EXPIRED
booking.show.release_seats(booking.seat_ids)
print("Booking expired! Seats released.")
return None
booking.show.confirm_seats(booking.seat_ids)
booking.status = BookingStatus.CONFIRMED
payment = Payment(booking, payment_method)
print(f"Booking {booking_id} confirmed! Payment: ${payment.amount}")
return payment
def cancel_booking(self, booking_id: str):
"""Cancel a confirmed booking."""
booking = self.bookings.get(booking_id)
if not booking:
return
booking.show.release_seats(booking.seat_ids)
booking.status = BookingStatus.CANCELLED
print(f"Booking {booking_id} cancelled. Seats released.")
# ---- Usage ----
# Setup
seats = ([Seat("A", i, SeatType.VIP) for i in range(1, 6)]
+ [Seat("B", i, SeatType.PREMIUM) for i in range(1, 11)]
+ [Seat("C", i, SeatType.REGULAR) for i in range(1, 16)])
screen = Screen(1, seats)
theater = Theater("PVR Phoenix", "Mumbai", [screen])
movie = Movie("Inception", 148, "Sci-Fi")
show = Show(movie, screen, datetime(2025, 1, 15, 18, 30))
# Booking flow
service = BookingService()
# User 1 selects seats
booking = service.create_booking("user_1", show, ["A1", "A2"])
# Booking abc123 created. Amount: $1000
# User 2 tries SAME seats -- fails!
booking2 = service.create_booking("user_2", show, ["A1", "A3"])
# Seats ['A1', 'A3'] not available -- someone beat us to it!
# User 1 pays
service.confirm_booking(booking.booking_id, "credit_card")
# Booking abc123 confirmed!
// ---- 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", EXPIRED: "EXPIRED"
});
const SEAT_PRICES = { [SeatType.REGULAR]: 200, [SeatType.PREMIUM]: 350, [SeatType.VIP]: 500 };
const LOCK_DURATION_MS = 5 * 60 * 1000; // 5 minutes
// ---- Models ----
class Movie {
constructor(title, durationMins, genre) {
this.movieId = crypto.randomUUID().slice(0, 8);
this.title = title;
this.durationMins = durationMins;
this.genre = genre;
}
}
class Seat {
constructor(row, number, seatType) {
this.seatId = `${row}${number}`;
this.row = row;
this.number = number;
this.seatType = seatType;
}
}
class Screen {
constructor(screenNumber, seats) {
this.screenNumber = screenNumber;
this.seats = seats;
}
}
// ---- Show (with seat locking) ----
class Show {
constructor(movie, screen, startTime) {
this.showId = crypto.randomUUID().slice(0, 8);
this.movie = movie;
this.screen = screen;
this.startTime = startTime;
this.seatStatus = new Map(screen.seats.map(s => [s.seatId, SeatStatus.AVAILABLE]));
this.seatLocks = new Map(); // seatId -> expiry timestamp
}
getAvailableSeats() {
this.#expireLocks();
return [...this.seatStatus.entries()]
.filter(([_, status]) => status === SeatStatus.AVAILABLE)
.map(([id]) => id);
}
lockSeats(seatIds) {
this.#expireLocks();
// Check all available first
if (seatIds.some(id => this.seatStatus.get(id) !== SeatStatus.AVAILABLE)) return false;
const expiry = Date.now() + LOCK_DURATION_MS;
seatIds.forEach(id => {
this.seatStatus.set(id, SeatStatus.LOCKED);
this.seatLocks.set(id, expiry);
});
return true;
}
confirmSeats(seatIds) {
seatIds.forEach(id => {
this.seatStatus.set(id, SeatStatus.BOOKED);
this.seatLocks.delete(id);
});
}
releaseSeats(seatIds) {
seatIds.forEach(id => {
this.seatStatus.set(id, SeatStatus.AVAILABLE);
this.seatLocks.delete(id);
});
}
#expireLocks() {
const now = Date.now();
for (const [id, expiry] of this.seatLocks) {
if (now > expiry) {
this.seatStatus.set(id, SeatStatus.AVAILABLE);
this.seatLocks.delete(id);
}
}
}
}
// ---- Booking & Payment ----
class Booking {
constructor(userId, show, seatIds) {
this.bookingId = crypto.randomUUID().slice(0, 8);
this.userId = userId;
this.show = show;
this.seatIds = seatIds;
this.status = BookingStatus.PENDING;
this.createdAt = Date.now();
this.amount = this.#calculateAmount();
}
#calculateAmount() {
const seatMap = Object.fromEntries(this.show.screen.seats.map(s => [s.seatId, s]));
return this.seatIds.reduce((sum, id) => sum + SEAT_PRICES[seatMap[id].seatType], 0);
}
}
// ---- Booking Service ----
class BookingService {
constructor() { this.bookings = new Map(); }
createBooking(userId, show, seatIds) {
if (!show.lockSeats(seatIds)) {
console.log(`Seats ${seatIds} not available!`);
return null;
}
const booking = new Booking(userId, show, seatIds);
this.bookings.set(booking.bookingId, booking);
console.log(`Booking ${booking.bookingId}: $${booking.amount}. Pay within 5 min.`);
return booking;
}
confirmBooking(bookingId, paymentMethod) {
const booking = this.bookings.get(bookingId);
if (!booking || booking.status !== BookingStatus.PENDING) return null;
if (Date.now() - booking.createdAt > LOCK_DURATION_MS) {
booking.status = BookingStatus.EXPIRED;
booking.show.releaseSeats(booking.seatIds);
console.log("Booking expired!");
return null;
}
booking.show.confirmSeats(booking.seatIds);
booking.status = BookingStatus.CONFIRMED;
console.log(`Booking ${bookingId} confirmed!`);
return { bookingId, amount: booking.amount, method: paymentMethod };
}
cancelBooking(bookingId) {
const booking = this.bookings.get(bookingId);
if (!booking) return;
booking.show.releaseSeats(booking.seatIds);
booking.status = BookingStatus.CANCELLED;
console.log(`Booking ${bookingId} cancelled.`);
}
}
// Usage
const seats = [
...Array.from({ length: 5 }, (_, i) => new Seat("A", i + 1, SeatType.VIP)),
...Array.from({ length: 10 }, (_, i) => new Seat("B", i + 1, SeatType.PREMIUM)),
...Array.from({ length: 15 }, (_, i) => new Seat("C", i + 1, SeatType.REGULAR)),
];
const screen = new Screen(1, seats);
const movie = new Movie("Inception", 148, "Sci-Fi");
const show = new Show(movie, screen, new Date("2025-01-15T18:30:00"));
const service = new BookingService();
const b1 = service.createBooking("user1", show, ["A1", "A2"]);
service.createBooking("user2", show, ["A1", "A3"]); // Fails!
service.confirmBooking(b1.bookingId, "credit_card");
import java.util.*;
import java.time.*;
import java.util.concurrent.ConcurrentHashMap;
enum SeatType { REGULAR, PREMIUM, VIP }
enum SeatStatus { AVAILABLE, LOCKED, BOOKED }
enum BookingStatus { PENDING, CONFIRMED, CANCELLED, EXPIRED }
class Movie {
String movieId = UUID.randomUUID().toString().substring(0, 8);
String title;
int durationMins;
Movie(String title, int mins) { this.title = title; this.durationMins = mins; }
}
class Seat {
String seatId;
String row;
int number;
SeatType seatType;
Seat(String row, int num, SeatType type) {
this.seatId = row + num;
this.row = row;
this.number = num;
this.seatType = type;
}
}
class Screen {
int screenNumber;
List<Seat> seats;
Screen(int num, List<Seat> seats) { this.screenNumber = num; this.seats = seats; }
}
class Show {
static final Duration LOCK_DURATION = Duration.ofMinutes(5);
String showId = UUID.randomUUID().toString().substring(0, 8);
Movie movie;
Screen screen;
LocalDateTime startTime;
ConcurrentHashMap<String, SeatStatus> seatStatus = new ConcurrentHashMap<>();
ConcurrentHashMap<String, Instant> seatLocks = new ConcurrentHashMap<>();
Show(Movie movie, Screen screen, LocalDateTime startTime) {
this.movie = movie;
this.screen = screen;
this.startTime = startTime;
screen.seats.forEach(s -> seatStatus.put(s.seatId, SeatStatus.AVAILABLE));
}
synchronized boolean lockSeats(List<String> seatIds) {
expireLocks();
for (String id : seatIds) {
if (seatStatus.get(id) != SeatStatus.AVAILABLE) return false;
}
Instant expiry = Instant.now().plus(LOCK_DURATION);
seatIds.forEach(id -> {
seatStatus.put(id, SeatStatus.LOCKED);
seatLocks.put(id, expiry);
});
return true;
}
synchronized void confirmSeats(List<String> ids) {
ids.forEach(id -> { seatStatus.put(id, SeatStatus.BOOKED); seatLocks.remove(id); });
}
synchronized void releaseSeats(List<String> ids) {
ids.forEach(id -> { seatStatus.put(id, SeatStatus.AVAILABLE); seatLocks.remove(id); });
}
private void expireLocks() {
Instant now = Instant.now();
seatLocks.forEach((id, expiry) -> {
if (now.isAfter(expiry)) {
seatStatus.put(id, SeatStatus.AVAILABLE);
seatLocks.remove(id);
}
});
}
}
class Booking {
String bookingId = UUID.randomUUID().toString().substring(0, 8);
String userId;
Show show;
List<String> seatIds;
BookingStatus status = BookingStatus.PENDING;
Instant createdAt = Instant.now();
double amount;
Booking(String userId, Show show, List<String> seatIds) {
this.userId = userId;
this.show = show;
this.seatIds = seatIds;
Map<SeatType, Integer> prices = Map.of(
SeatType.REGULAR, 200, SeatType.PREMIUM, 350, SeatType.VIP, 500);
Map<String, Seat> seatMap = new HashMap<>();
show.screen.seats.forEach(s -> seatMap.put(s.seatId, s));
this.amount = seatIds.stream()
.mapToInt(id -> prices.get(seatMap.get(id).seatType)).sum();
}
}
class BookingService {
Map<String, Booking> bookings = new ConcurrentHashMap<>();
Booking createBooking(String userId, Show show, List<String> seatIds) {
if (!show.lockSeats(seatIds)) {
System.out.println("Seats not available!");
return null;
}
Booking b = new Booking(userId, show, seatIds);
bookings.put(b.bookingId, b);
System.out.printf("Booking %s: $%.0f. Pay within 5 min.%n", b.bookingId, b.amount);
return b;
}
boolean confirmBooking(String bookingId) {
Booking b = bookings.get(bookingId);
if (b == null || b.status != BookingStatus.PENDING) return false;
if (Instant.now().isAfter(b.createdAt.plus(Show.LOCK_DURATION))) {
b.status = BookingStatus.EXPIRED;
b.show.releaseSeats(b.seatIds);
return false;
}
b.show.confirmSeats(b.seatIds);
b.status = BookingStatus.CONFIRMED;
System.out.printf("Booking %s confirmed!%n", bookingId);
return true;
}
}
The Seat Locking Problem
This is the part interviewers really care about. Here’s the flow:
- User selects seats — we call
lockSeats(). This atomically checks all seats are available and locks them. - User has 5 minutes to complete payment. During this time, no one else can book those seats.
- User pays — we call
confirmSeats(). Seats go from LOCKED to BOOKED. Done. - User doesn’t pay in time —
_expire_locks()runs on the next request and releases the seats back to AVAILABLE.
The synchronized keyword (Java) or Lock (Python) is critical here. Without it, two threads could both check that seat A1 is available, both see True, and both try to lock it. Classic race condition.
In simple language, it’s like putting a “hold” sticker on items at a store. We hold them at the counter for 5 minutes while the customer goes to get their wallet. If they don’t come back, items go back on the shelf.
Design Patterns Used
Repository/Service pattern — BookingService manages the booking lifecycle. Show manages seat state. Clean separation of concerns.
Concurrency control — Pessimistic locking via synchronized blocks. We lock first, ask questions later. This prevents double-booking at the cost of some waiting.
Enum-based state machine — SeatStatus transitions: AVAILABLE -> LOCKED -> BOOKED (or back to AVAILABLE on expiry/cancellation).
Extensions
- Offers/Coupons — Add a
CouponServicethat validates and applies discounts before payment. Strategy pattern for different discount types (flat, percentage, BOGO). - Food ordering — Add
FoodItemandFoodOrderclasses. Attach a food order to a Booking. Process food payment together with ticket payment. - Cancellation with refund policy — Time-based refund: full refund if 24h before show, 50% if 4h before, no refund after. Strategy pattern for refund calculation.
- Waitlist — If all seats are booked, let users join a waitlist. Observer pattern: notify waitlisted users when seats are released.
- Seat categories with dynamic pricing — Weekend shows cost more, morning shows cost less. Add a
PricingStrategythat takes show time, day, and demand into account.