Low-Level Design: Movie Ticket Booking System (OOP Interview)

Low-Level Design: Movie Ticket Booking System

The movie ticket booking system (like BookMyShow or Fandango) tests: seat reservation logic, concurrency handling, the booking lifecycle, and pricing tiers. It’s a natural fit for the OOP interview because it has clear entities (Movie, Show, Seat, Booking) with well-defined relationships and state transitions.

Core Classes

Enums

from enum import Enum

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

class SeatStatus(Enum):
    AVAILABLE = "AVAILABLE"
    HELD = "HELD"       # temporarily locked during payment
    BOOKED = "BOOKED"

class BookingStatus(Enum):
    PENDING = "PENDING"       # seats held, payment not complete
    CONFIRMED = "CONFIRMED"   # payment successful
    CANCELLED = "CANCELLED"
    EXPIRED = "EXPIRED"       # hold timed out

Movie and Theater

from dataclasses import dataclass, field
from datetime import datetime

@dataclass
class Movie:
    movie_id: str
    title: str
    duration_minutes: int
    rating: str   # G, PG, PG-13, R

@dataclass
class Theater:
    theater_id: str
    name: str
    location: str

@dataclass
class Seat:
    seat_id: str
    row: str
    number: int
    seat_type: SeatType
    status: SeatStatus = SeatStatus.AVAILABLE

    @property
    def label(self) -> str:
        return f"{self.row}{self.number}"

Show (Screening)

@dataclass
class Show:
    show_id: str
    movie: Movie
    theater: Theater
    start_time: datetime
    seats: dict[str, Seat] = field(default_factory=dict)  # seat_id -> Seat
    base_prices: dict[SeatType, float] = field(default_factory=lambda: {
        SeatType.REGULAR:  12.0,
        SeatType.PREMIUM:  18.0,
        SeatType.RECLINER: 25.0,
    })

    def add_seat(self, seat: Seat) -> None:
        self.seats[seat.seat_id] = seat

    def available_seats(self, seat_type: SeatType = None) -> list[Seat]:
        return [
            s for s in self.seats.values()
            if s.status == SeatStatus.AVAILABLE
            and (seat_type is None or s.seat_type == seat_type)
        ]

    def price_for(self, seat: Seat) -> float:
        """Price with time-of-day surcharge."""
        base = self.base_prices[seat.seat_type]
        if self.start_time.hour >= 18:   # evening surcharge
            base *= 1.15
        return round(base, 2)

Booking

import uuid
from datetime import datetime, timedelta

HOLD_DURATION_MINUTES = 10

@dataclass
class Booking:
    booking_id: str
    user_id: str
    show: Show
    seats: list[Seat]
    status: BookingStatus = BookingStatus.PENDING
    created_at: datetime = field(default_factory=datetime.now)
    hold_expires_at: datetime = None

    def __post_init__(self):
        if self.hold_expires_at is None:
            self.hold_expires_at = self.created_at + timedelta(minutes=HOLD_DURATION_MINUTES)

    @property
    def total_price(self) -> float:
        return round(sum(self.show.price_for(seat) for seat in self.seats), 2)

    @property
    def is_hold_expired(self) -> bool:
        return datetime.now() > self.hold_expires_at

    def confirm(self) -> None:
        if self.status != BookingStatus.PENDING:
            raise ValueError(f"Cannot confirm booking in status {self.status.value}")
        if self.is_hold_expired:
            self.status = BookingStatus.EXPIRED
            raise ValueError("Booking hold has expired")
        self.status = BookingStatus.CONFIRMED
        for seat in self.seats:
            seat.status = SeatStatus.BOOKED

    def cancel(self) -> None:
        if self.status == BookingStatus.CONFIRMED:
            for seat in self.seats:
                seat.status = SeatStatus.AVAILABLE
        self.status = BookingStatus.CANCELLED

BookingSystem (Orchestrator)

import threading

class BookingSystem:
    def __init__(self):
        self.shows: dict[str, Show] = {}
        self.bookings: dict[str, Booking] = {}
        self._seat_locks: dict[str, threading.Lock] = {}   # show_id -> Lock

    def add_show(self, show: Show) -> None:
        self.shows[show.show_id] = show
        self._seat_locks[show.show_id] = threading.Lock()

    def hold_seats(self, user_id: str, show_id: str, seat_ids: list[str]) -> Booking:
        """
        Atomically check availability and hold seats.
        Lock ensures no two users can hold the same seat simultaneously.
        """
        show = self.shows.get(show_id)
        if not show:
            raise ValueError(f"Show {show_id} not found")

        with self._seat_locks[show_id]:
            # Verify all requested seats are available
            seats_to_hold = []
            for seat_id in seat_ids:
                seat = show.seats.get(seat_id)
                if not seat:
                    raise ValueError(f"Seat {seat_id} not found")
                if seat.status != SeatStatus.AVAILABLE:
                    raise ValueError(f"Seat {seat.label} is not available")
                seats_to_hold.append(seat)

            # Mark seats as HELD
            for seat in seats_to_hold:
                seat.status = SeatStatus.HELD

        booking = Booking(
            booking_id=str(uuid.uuid4()),
            user_id=user_id,
            show=show,
            seats=seats_to_hold,
        )
        self.bookings[booking.booking_id] = booking
        print(f"Held {len(seats_to_hold)} seats. Booking: {booking.booking_id} "
              f"(expires in {HOLD_DURATION_MINUTES} min)")
        return booking

    def confirm_booking(self, booking_id: str) -> None:
        booking = self.bookings.get(booking_id)
        if not booking:
            raise ValueError(f"Booking {booking_id} not found")
        booking.confirm()
        print(f"Booking {booking_id} confirmed. Total: $" + f"{booking.total_price:.2f}")

    def cancel_booking(self, booking_id: str) -> None:
        booking = self.bookings.get(booking_id)
        if not booking:
            raise ValueError(f"Booking {booking_id} not found")
        booking.cancel()
        print(f"Booking {booking_id} cancelled. Seats released.")

    def release_expired_holds(self) -> int:
        """Called periodically to release timed-out HELD seats."""
        released = 0
        for booking in self.bookings.values():
            if booking.status == BookingStatus.PENDING and booking.is_hold_expired:
                booking.status = BookingStatus.EXPIRED
                for seat in booking.seats:
                    if seat.status == SeatStatus.HELD:
                        seat.status = SeatStatus.AVAILABLE
                released += 1
        return released

Usage Example

from datetime import datetime

system = BookingSystem()
movie = Movie("M1", "Inception", 148, "PG-13")
theater = Theater("T1", "AMC Downtown", "NYC")

show = Show("S1", movie, theater, datetime(2026, 5, 1, 20, 0))
for row in "ABCDE":
    for num in range(1, 11):
        stype = SeatType.RECLINER if row == "E" else SeatType.PREMIUM if row in "CD" else SeatType.REGULAR
        show.add_seat(Seat(f"{row}{num}", row, num, stype))

system.add_show(show)

# Book tickets
booking = system.hold_seats("user_42", "S1", ["A1", "A2", "A3"])
system.confirm_booking(booking.booking_id)

# Show availability
print(f"Available regular seats: {len(show.available_seats(SeatType.REGULAR))}")

Interview Follow-ups

  • Why threading.Lock per show? Locking at the show level (not system level) maximizes concurrency — users booking different shows never block each other.
  • Distributed concurrency: Replace threading.Lock with Redis SETNX lock per show. Or use SELECT FOR UPDATE in PostgreSQL to lock rows during seat availability check.
  • Seat expiry: Run release_expired_holds() in a background thread every 30 seconds, or use Redis key TTL (set seat HELD status with TTL; Redis auto-expires it).
  • Waitlist: When a booked seat is cancelled, notify the first user on the waitlist and give them a 5-minute hold window.

{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you prevent double-booking of seats in a movie ticket system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use a lock per show (not per seat, not a global lock) to maximize concurrency. When hold_seats() is called: acquire the show’s lock, verify all requested seats are AVAILABLE, mark them as HELD, release the lock. This is atomic — no two users can hold the same seat. In a single-process system: use threading.Lock per show_id. In a distributed system: use Redis SETNX to create a “show_hold_lock:{show_id}” key with a short TTL; the process that successfully sets the key proceeds with the booking; others retry or fail. Alternative: use a database transaction with SELECT FOR UPDATE on the seat rows — the DB serializes concurrent transactions at the row level, giving correct isolation without application-level locking. The hold pattern (HELD status with a 10-minute expiry) separates the seat reservation from payment completion, avoiding permanently locking seats while users are entering credit card details.”}},{“@type”:”Question”,”name”:”How do you implement seat hold expiry in a movie ticket booking system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Two approaches: (1) Polling — a background thread runs every 30 seconds, scans all PENDING bookings, checks if hold_expires_at < now, marks expired bookings as EXPIRED, and releases (sets status back to AVAILABLE) the associated seats. Simple but has up to 30 seconds of delay. (2) Redis TTL — when marking a seat as HELD, store the seat status in Redis with a TTL equal to the hold duration. When the TTL expires, Redis auto-deletes the key; the next availability check reads AVAILABLE from the database. This requires a dual-source design (Redis for live status, DB as source of truth). In practice, a hybrid approach works well: use polling for cleanup (correctness) and Redis TTL for read-path performance (avoid serving stale HELD status to other users). Regardless of approach: when a user completes payment (confirm()), check hold_expires_at before marking CONFIRMED — reject expired holds even if the cleanup thread hasn't run yet."}},{"@type":"Question","name":"How do you design pricing with dynamic tiers in a movie ticket booking system?","acceptedAnswer":{"@type":"Answer","text":"Separate pricing into three dimensions: seat type (Regular < Premium float. Inject SeasonalPricingStrategy, DemandBasedPricingStrategy, or FlatRatePricingStrategy. The Booking.total_price property sums price_for() across all booked seats — this ensures the price is locked at booking time and does not change if the pricing strategy changes later.”}}]}

🏢 Asked at: Snap Interview Guide

🏢 Asked at: Shopify Interview Guide

🏢 Asked at: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

🏢 Asked at: Airbnb Interview Guide 2026: Search Systems, Trust and Safety, and Full-Stack Engineering

🏢 Asked at: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering

Asked at: Atlassian Interview Guide

Scroll to Top