Design BookMyShow

advanced 4-7 YOE lld bookmyshow design-question concurrency

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

BookMyShow Class Diagram
Movie
title, duration, genre
Theater
name, city, screens[]
Screen
screenNumber, seats[]
│ a Show ties Movie + Screen + Time
Show
movie, screen, startTime
seat_status: Map<Seat, SeatStatus>
│ booking locks seats
Booking
show, seats[], status
lock_time, user
Seat
row, number, type
REGULAR / PREMIUM / VIP
Payment
amount, method
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:

  1. User selects seats — we call lockSeats(). This atomically checks all seats are available and locks them.
  2. User has 5 minutes to complete payment. During this time, no one else can book those seats.
  3. User pays — we call confirmSeats(). Seats go from LOCKED to BOOKED. Done.
  4. 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 CouponService that validates and applies discounts before payment. Strategy pattern for different discount types (flat, percentage, BOGO).
  • Food ordering — Add FoodItem and FoodOrder classes. 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 PricingStrategy that takes show time, day, and demand into account.