Skip to content
Unverified — AI-generated content. Help verify this page

Design Movie Ticket Booking (LLD)

Movie ticket booking is one of the most commonly asked LLD problems because it combines several challenging concepts: entity modeling, concurrency control (no double-booking), pricing logic, and temporal constraints (show times, seat locks). The core challenge is ensuring that two users cannot book the same seat simultaneously — a problem that maps directly to real-world production systems.

Requirements

Functional Requirements

#RequirementDetails
FR-1Browse moviesSearch by city, genre, language
FR-2View showtimesList available shows for a movie at a theater
FR-3Select seatsDisplay seat map, allow seat selection
FR-4Seat lockingTemporarily lock seats during checkout (TTL-based)
FR-5Book ticketsConfirm booking after payment
FR-6Cancel bookingCancel and release seats with refund policy
FR-7PricingBase price + premium surcharge + convenience fee

Non-Functional Requirements

  • No double booking (strong consistency on seat allocation)
  • Seat lock expires after timeout (e.g., 10 minutes)
  • Support concurrent users selecting seats for the same show

Class Diagram

Core Implementation

Enums and Types

TypeScript:

typescript
enum SeatType {
  REGULAR = "REGULAR",
  PREMIUM = "PREMIUM",
  VIP = "VIP",
  RECLINER = "RECLINER",
}

enum BookingStatus {
  PENDING = "PENDING",
  CONFIRMED = "CONFIRMED",
  CANCELLED = "CANCELLED",
  EXPIRED = "EXPIRED",
}

enum Genre {
  ACTION = "ACTION",
  COMEDY = "COMEDY",
  DRAMA = "DRAMA",
  HORROR = "HORROR",
  SCIFI = "SCIFI",
  THRILLER = "THRILLER",
}

Python:

python
from enum import Enum
from dataclasses import dataclass, field
from datetime import datetime, timedelta
import uuid
import threading

class SeatType(Enum):
    REGULAR = "REGULAR"
    PREMIUM = "PREMIUM"
    VIP = "VIP"
    RECLINER = "RECLINER"

class BookingStatus(Enum):
    PENDING = "PENDING"
    CONFIRMED = "CONFIRMED"
    CANCELLED = "CANCELLED"
    EXPIRED = "EXPIRED"

class Genre(Enum):
    ACTION = "ACTION"
    COMEDY = "COMEDY"
    DRAMA = "DRAMA"
    HORROR = "HORROR"
    SCIFI = "SCIFI"
    THRILLER = "THRILLER"

Entity Classes

TypeScript:

typescript
class Movie {
  constructor(
    public readonly id: string,
    public readonly title: string,
    public readonly genre: Genre,
    public readonly language: string,
    public readonly durationMinutes: number,
    public readonly rating: number = 0
  ) {}
}

class Seat {
  constructor(
    public readonly id: string,
    public readonly row: string,
    public readonly number: number,
    public readonly type: SeatType
  ) {}
}

class Hall {
  private seats: Seat[] = [];

  constructor(
    public readonly id: string,
    public readonly name: string
  ) {}

  addSeat(seat: Seat): void {
    this.seats.push(seat);
  }

  getSeats(): Seat[] {
    return [...this.seats];
  }

  get totalCapacity(): number {
    return this.seats.length;
  }
}

class Theater {
  private halls: Hall[] = [];

  constructor(
    public readonly id: string,
    public readonly name: string,
    public readonly city: string
  ) {}

  addHall(hall: Hall): void {
    this.halls.push(hall);
  }

  getHalls(): Hall[] {
    return [...this.halls];
  }
}

Python:

python
@dataclass
class Movie:
    id: str
    title: str
    genre: Genre
    language: str
    duration_minutes: int
    rating: float = 0.0

@dataclass
class Seat:
    id: str
    row: str
    number: int
    type: SeatType

@dataclass
class Hall:
    id: str
    name: str
    seats: list[Seat] = field(default_factory=list)

    def add_seat(self, seat: Seat) -> None:
        self.seats.append(seat)

    @property
    def total_capacity(self) -> int:
        return len(self.seats)

@dataclass
class Theater:
    id: str
    name: str
    city: str
    halls: list[Hall] = field(default_factory=list)

    def add_hall(self, hall: Hall) -> None:
        self.halls.append(hall)

Pricing Strategy

Pricing uses the Strategy pattern to allow different pricing models per show (weekday vs weekend, standard vs IMAX, etc.).

Total=i=1n(basePrice×seatMultiplier(si))+n×convenienceFee

TypeScript:

typescript
interface PricingStrategy {
  calculatePrice(seats: Seat[]): number;
}

class StandardPricing implements PricingStrategy {
  private basePrice: number;
  private convenienceFee: number;
  private seatMultipliers: Map<SeatType, number>;

  constructor(basePrice: number, convenienceFee: number = 30) {
    this.basePrice = basePrice;
    this.convenienceFee = convenienceFee;
    this.seatMultipliers = new Map([
      [SeatType.REGULAR, 1.0],
      [SeatType.PREMIUM, 1.5],
      [SeatType.VIP, 2.0],
      [SeatType.RECLINER, 2.5],
    ]);
  }

  calculatePrice(seats: Seat[]): number {
    let total = 0;
    for (const seat of seats) {
      const multiplier = this.seatMultipliers.get(seat.type) ?? 1.0;
      total += this.basePrice * multiplier;
    }
    total += seats.length * this.convenienceFee;
    return Math.round(total * 100) / 100;
  }
}

class WeekendPricing implements PricingStrategy {
  private standardPricing: StandardPricing;
  private surchargePercent: number;

  constructor(basePrice: number, surchargePercent = 20) {
    this.standardPricing = new StandardPricing(basePrice);
    this.surchargePercent = surchargePercent;
  }

  calculatePrice(seats: Seat[]): number {
    const base = this.standardPricing.calculatePrice(seats);
    return Math.round(base * (1 + this.surchargePercent / 100) * 100) / 100;
  }
}

Python:

python
class PricingStrategy(ABC):
    @abstractmethod
    def calculate_price(self, seats: list[Seat]) -> float: ...

class StandardPricing(PricingStrategy):
    SEAT_MULTIPLIERS = {
        SeatType.REGULAR: 1.0,
        SeatType.PREMIUM: 1.5,
        SeatType.VIP: 2.0,
        SeatType.RECLINER: 2.5,
    }

    def __init__(self, base_price: float, convenience_fee: float = 30.0):
        self.base_price = base_price
        self.convenience_fee = convenience_fee

    def calculate_price(self, seats: list[Seat]) -> float:
        total = sum(
            self.base_price * self.SEAT_MULTIPLIERS.get(s.type, 1.0)
            for s in seats
        )
        total += len(seats) * self.convenience_fee
        return round(total, 2)

class WeekendPricing(PricingStrategy):
    def __init__(self, base_price: float, surcharge_percent: float = 20.0):
        self._standard = StandardPricing(base_price)
        self._surcharge = surcharge_percent

    def calculate_price(self, seats: list[Seat]) -> float:
        base = self._standard.calculate_price(seats)
        return round(base * (1 + self._surcharge / 100), 2)

Show and Seat Lock Management

The seat lock mechanism is the most critical part of this design. When a user selects seats, those seats are temporarily locked for a configurable TTL. If the user does not complete payment within the TTL, the locks expire and seats become available again.

TypeScript:

typescript
class SeatLock {
  constructor(
    public readonly seat: Seat,
    public readonly showId: string,
    public readonly userId: string,
    public readonly expiresAt: Date
  ) {}

  isExpired(): boolean {
    return new Date() > this.expiresAt;
  }
}

class Show {
  private bookedSeats: Set<string> = new Set(); // seat IDs
  private seatLocks: Map<string, SeatLock> = new Map(); // seatId -> lock

  constructor(
    public readonly id: string,
    public readonly movie: Movie,
    public readonly hall: Hall,
    public readonly startTime: Date,
    public readonly endTime: Date,
    public readonly pricing: PricingStrategy
  ) {}

  getAvailableSeats(): Seat[] {
    this.cleanExpiredLocks();
    return this.hall.getSeats().filter(
      (seat) => !this.bookedSeats.has(seat.id) && !this.seatLocks.has(seat.id)
    );
  }

  lockSeats(seats: Seat[], userId: string, ttlMinutes = 10): boolean {
    this.cleanExpiredLocks();

    // Check all seats are available
    for (const seat of seats) {
      if (this.bookedSeats.has(seat.id) || this.seatLocks.has(seat.id)) {
        return false;
      }
    }

    // Lock all seats atomically
    const expiresAt = new Date(Date.now() + ttlMinutes * 60 * 1000);
    for (const seat of seats) {
      this.seatLocks.set(
        seat.id,
        new SeatLock(seat, this.id, userId, expiresAt)
      );
    }

    return true;
  }

  confirmBooking(seats: Seat[], userId: string): boolean {
    // Verify locks belong to this user
    for (const seat of seats) {
      const lock = this.seatLocks.get(seat.id);
      if (!lock || lock.userId !== userId || lock.isExpired()) {
        return false;
      }
    }

    // Confirm: move from locked to booked
    for (const seat of seats) {
      this.seatLocks.delete(seat.id);
      this.bookedSeats.add(seat.id);
    }

    return true;
  }

  releaseLocks(seats: Seat[], userId: string): void {
    for (const seat of seats) {
      const lock = this.seatLocks.get(seat.id);
      if (lock && lock.userId === userId) {
        this.seatLocks.delete(seat.id);
      }
    }
  }

  cancelSeats(seats: Seat[]): void {
    for (const seat of seats) {
      this.bookedSeats.delete(seat.id);
    }
  }

  private cleanExpiredLocks(): void {
    for (const [seatId, lock] of this.seatLocks) {
      if (lock.isExpired()) {
        this.seatLocks.delete(seatId);
      }
    }
  }
}

Python:

python
@dataclass
class SeatLock:
    seat: Seat
    show_id: str
    user_id: str
    expires_at: datetime

    def is_expired(self) -> bool:
        return datetime.now() > self.expires_at

class Show:
    def __init__(self, id: str, movie: Movie, hall: Hall,
                 start_time: datetime, end_time: datetime,
                 pricing: PricingStrategy):
        self.id = id
        self.movie = movie
        self.hall = hall
        self.start_time = start_time
        self.end_time = end_time
        self.pricing = pricing
        self._booked_seats: set[str] = set()
        self._seat_locks: dict[str, SeatLock] = {}
        self._lock = threading.Lock()

    def get_available_seats(self) -> list[Seat]:
        self._clean_expired_locks()
        return [
            s for s in self.hall.seats
            if s.id not in self._booked_seats and s.id not in self._seat_locks
        ]

    def lock_seats(self, seats: list[Seat], user_id: str,
                   ttl_minutes: int = 10) -> bool:
        with self._lock:
            self._clean_expired_locks()

            for seat in seats:
                if seat.id in self._booked_seats or seat.id in self._seat_locks:
                    return False

            expires_at = datetime.now() + timedelta(minutes=ttl_minutes)
            for seat in seats:
                self._seat_locks[seat.id] = SeatLock(
                    seat=seat, show_id=self.id,
                    user_id=user_id, expires_at=expires_at
                )
            return True

    def confirm_booking(self, seats: list[Seat], user_id: str) -> bool:
        with self._lock:
            for seat in seats:
                lock = self._seat_locks.get(seat.id)
                if not lock or lock.user_id != user_id or lock.is_expired():
                    return False

            for seat in seats:
                del self._seat_locks[seat.id]
                self._booked_seats.add(seat.id)
            return True

    def release_locks(self, seats: list[Seat], user_id: str) -> None:
        with self._lock:
            for seat in seats:
                lock = self._seat_locks.get(seat.id)
                if lock and lock.user_id == user_id:
                    del self._seat_locks[seat.id]

    def cancel_seats(self, seats: list[Seat]) -> None:
        with self._lock:
            for seat in seats:
                self._booked_seats.discard(seat.id)

    def _clean_expired_locks(self) -> None:
        expired = [sid for sid, lock in self._seat_locks.items() if lock.is_expired()]
        for sid in expired:
            del self._seat_locks[sid]

Booking Service

TypeScript:

typescript
class Booking {
  public status: BookingStatus = BookingStatus.PENDING;
  public readonly createdAt = new Date();

  constructor(
    public readonly id: string,
    public readonly show: Show,
    public readonly seats: Seat[],
    public readonly userId: string,
    public readonly totalAmount: number
  ) {}
}

class BookingService {
  private bookings: Map<string, Booking> = new Map();

  initiateBooking(show: Show, seats: Seat[], userId: string): Booking | null {
    // Lock seats
    if (!show.lockSeats(seats, userId)) {
      return null; // seats unavailable
    }

    const totalAmount = show.pricing.calculatePrice(seats);
    const booking = new Booking(
      crypto.randomUUID(),
      show,
      seats,
      userId,
      totalAmount
    );

    this.bookings.set(booking.id, booking);
    return booking;
  }

  confirmBooking(bookingId: string): boolean {
    const booking = this.bookings.get(bookingId);
    if (!booking || booking.status !== BookingStatus.PENDING) {
      return false;
    }

    if (booking.show.confirmBooking(booking.seats, booking.userId)) {
      booking.status = BookingStatus.CONFIRMED;
      return true;
    }

    // Lock expired
    booking.status = BookingStatus.EXPIRED;
    return false;
  }

  cancelBooking(bookingId: string): boolean {
    const booking = this.bookings.get(bookingId);
    if (!booking || booking.status !== BookingStatus.CONFIRMED) {
      return false;
    }

    booking.show.cancelSeats(booking.seats);
    booking.status = BookingStatus.CANCELLED;
    return true;
  }
}

Python:

python
@dataclass
class Booking:
    id: str
    show: Show
    seats: list[Seat]
    user_id: str
    total_amount: float
    status: BookingStatus = BookingStatus.PENDING
    created_at: datetime = field(default_factory=datetime.now)

class BookingService:
    def __init__(self):
        self._bookings: dict[str, Booking] = {}

    def initiate_booking(self, show: Show, seats: list[Seat],
                         user_id: str) -> Booking | None:
        if not show.lock_seats(seats, user_id):
            return None

        total = show.pricing.calculate_price(seats)
        booking = Booking(
            id=str(uuid.uuid4()),
            show=show,
            seats=seats,
            user_id=user_id,
            total_amount=total
        )
        self._bookings[booking.id] = booking
        return booking

    def confirm_booking(self, booking_id: str) -> bool:
        booking = self._bookings.get(booking_id)
        if not booking or booking.status != BookingStatus.PENDING:
            return False

        if booking.show.confirm_booking(booking.seats, booking.user_id):
            booking.status = BookingStatus.CONFIRMED
            return True

        booking.status = BookingStatus.EXPIRED
        return False

    def cancel_booking(self, booking_id: str) -> bool:
        booking = self._bookings.get(booking_id)
        if not booking or booking.status != BookingStatus.CONFIRMED:
            return False

        booking.show.cancel_seats(booking.seats)
        booking.status = BookingStatus.CANCELLED
        return True

Concurrency: The Double-Booking Problem

WARNING

The seat locking mechanism must be atomic. If locking multiple seats, either all succeed or none do. A partial lock (where only some seats are locked) leads to inconsistencies and a poor user experience.

Design Patterns Used

PatternWhere UsedWhy
StrategyPricingStrategyDifferent pricing for weekdays, weekends, IMAX, etc.
BuilderConstructing Hall with seatsHalls have many seats with different types
RepositoryBookingService storageAbstracts persistence layer
ObserverBooking status changesNotify user via email/SMS on confirmation/cancellation

Complexity Analysis

OperationTimeSpace
Get available seatsO(S) where S = total seatsO(S)
Lock seatsO(K) where K = selected seatsO(K)
Confirm bookingO(K)O(1)
Clean expired locksO(L) where L = active locksO(1)
Calculate priceO(K)O(1)

Extensions to Discuss

  • Seat map visualization (2D grid rendering with status colors)
  • Waitlist when show is sold out (Observer pattern for notifications)
  • Coupon/promo codes (Decorator pattern on PricingStrategy)
  • Recurring bookings (season passes)
  • Refund policy (time-based: full refund > 24h, 50% > 4h, no refund < 4h)
  • Payment integration (Strategy pattern for payment methods)

Further Reading

"What I cannot create, I do not understand." — Richard Feynman