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) is a popular LLD interview question that covers seat reservation races, payment flow, and time-bound holds. It combines OOP design with concurrency challenges.

Requirements

  • Browse movies and showtimes at different theaters
  • View seat map and select seats
  • Hold selected seats temporarily (10-minute reservation window)
  • Complete booking with payment
  • Cancel booking (seat released back to inventory)
  • Support different seat categories: Regular, Premium, Recliner

Core Classes

Movie, Theater, and Showtime

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

class SeatCategory(Enum):
    REGULAR = "regular"
    PREMIUM = "premium"
    RECLINER = "recliner"

class SeatStatus(Enum):
    AVAILABLE = "available"
    HELD = "held"        # Temporarily reserved; expires after timeout
    BOOKED = "booked"    # Payment confirmed

@dataclass
class Movie:
    movie_id: str
    title: str
    duration_minutes: int
    genre: str
    rating: str  # PG, PG-13, R, etc.
    language: str = "English"

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

@dataclass
class Seat:
    seat_id: str
    row: str           # A, B, C...
    number: int        # 1, 2, 3...
    category: SeatCategory
    price: float
    status: SeatStatus = SeatStatus.AVAILABLE
    held_by: Optional[str] = None   # user_id
    held_until: Optional[datetime] = None
    booked_by: Optional[str] = None

    def __str__(self):
        return f"{self.row}{self.number}"

    def is_available(self) -> bool:
        if self.status == SeatStatus.AVAILABLE:
            return True
        if self.status == SeatStatus.HELD:
            # Check if hold has expired
            if self.held_until and datetime.utcnow() > self.held_until:
                self.status = SeatStatus.AVAILABLE
                self.held_by = None
                self.held_until = None
                return True
        return False

@dataclass
class Auditorium:
    auditorium_id: str
    theater_id: str
    name: str  # Hall 1, IMAX, etc.
    total_seats: int
    seats: dict = field(default_factory=dict)  # seat_id -> Seat

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

    def get_available_seats(self) -> list[Seat]:
        return [s for s in self.seats.values() if s.is_available()]

    def get_seat_map(self) -> dict:
        """Return seat status grid for display."""
        rows = {}
        for seat in self.seats.values():
            if seat.row not in rows:
                rows[seat.row] = {}
            rows[seat.row][seat.number] = {
                'seat_id': seat.seat_id,
                'category': seat.category.value,
                'price': seat.price,
                'status': 'available' if seat.is_available() else seat.status.value,
            }
        return rows

@dataclass
class Showtime:
    showtime_id: str
    movie: Movie
    auditorium: Auditorium
    theater: Theater
    start_time: datetime
    end_time: datetime
    _lock: threading.RLock = field(default_factory=threading.RLock, repr=False)

    @property
    def available_seats(self) -> list[Seat]:
        return self.auditorium.get_available_seats()

Booking and Hold Management

HOLD_DURATION_MINUTES = 10

@dataclass
class Booking:
    booking_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8].upper())
    user_id: str = ""
    showtime_id: str = ""
    seat_ids: list[str] = field(default_factory=list)
    total_amount: float = 0.0
    status: str = "confirmed"  # confirmed, cancelled
    created_at: datetime = field(default_factory=datetime.utcnow)
    payment_ref: str = ""

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

    def add_showtime(self, showtime: Showtime):
        self._showtimes[showtime.showtime_id] = showtime

    def hold_seats(self, user_id: str, showtime_id: str,
                   seat_ids: list[str]) -> dict:
        """
        Atomically hold multiple seats. All-or-nothing: if any seat
        is unavailable, no seats are held.
        """
        showtime = self._showtimes.get(showtime_id)
        if not showtime:
            raise ValueError("Showtime not found")

        with showtime._lock:
            # Validate all seats available before holding any
            seats = []
            for seat_id in seat_ids:
                seat = showtime.auditorium.seats.get(seat_id)
                if not seat:
                    raise ValueError(f"Seat {seat_id} not found")
                if not seat.is_available():
                    raise ValueError(f"Seat {seat} is no longer available")
                seats.append(seat)

            # All available: hold them atomically
            expiry = datetime.utcnow() + timedelta(minutes=HOLD_DURATION_MINUTES)
            for seat in seats:
                seat.status = SeatStatus.HELD
                seat.held_by = user_id
                seat.held_until = expiry

        total = sum(s.price for s in seats)
        return {
            'hold_id': str(uuid.uuid4()),
            'seat_ids': seat_ids,
            'total': total,
            'expires_at': expiry.isoformat(),
            'payment_required_by': expiry.isoformat(),
        }

    def confirm_booking(self, user_id: str, showtime_id: str,
                        seat_ids: list[str], payment_ref: str) -> Booking:
        showtime = self._showtimes[showtime_id]
        with showtime._lock:
            seats = []
            for seat_id in seat_ids:
                seat = showtime.auditorium.seats[seat_id]
                if seat.status != SeatStatus.HELD or seat.held_by != user_id:
                    raise ValueError(f"Seat {seat} is not held by this user")
                if seat.held_until and datetime.utcnow() > seat.held_until:
                    raise ValueError(f"Hold on seat {seat} has expired")
                seats.append(seat)

            # Process payment (real system: call payment gateway)
            if not payment_service.charge(user_id, sum(s.price for s in seats), payment_ref):
                raise ValueError("Payment failed")

            # Confirm booking
            for seat in seats:
                seat.status = SeatStatus.BOOKED
                seat.booked_by = user_id
                seat.held_by = None
                seat.held_until = None

        booking = Booking(
            user_id=user_id,
            showtime_id=showtime_id,
            seat_ids=seat_ids,
            total_amount=sum(s.price for s in seats),
            payment_ref=payment_ref,
        )
        bookings_db.save(booking)
        return booking

    def cancel_booking(self, booking_id: str, user_id: str) -> dict:
        booking = bookings_db.get(booking_id)
        if not booking or booking.user_id != user_id:
            raise ValueError("Booking not found")
        if booking.status == "cancelled":
            raise ValueError("Already cancelled")

        showtime = self._showtimes[booking.showtime_id]
        with showtime._lock:
            for seat_id in booking.seat_ids:
                seat = showtime.auditorium.seats[seat_id]
                seat.status = SeatStatus.AVAILABLE
                seat.booked_by = None

        booking.status = "cancelled"
        bookings_db.save(booking)

        # Refund (minus cancellation fee if applicable)
        refund_amount = self._calculate_refund(booking, showtime.start_time)
        payment_service.refund(booking.payment_ref, refund_amount)
        return {'refund': refund_amount}

    def _calculate_refund(self, booking: Booking, show_start: datetime) -> float:
        hours_until_show = (show_start - datetime.utcnow()).total_seconds() / 3600
        if hours_until_show > 24:
            return booking.total_amount  # Full refund
        elif hours_until_show > 4:
            return booking.total_amount * 0.5  # 50% refund
        return 0.0  # No refund within 4 hours

Search and Discovery

class MovieSearchService:
    def search_movies(self, city: str, date: str,
                      genre: str = None, language: str = None) -> list[dict]:
        """Find movies with available showtimes in a city on a date."""
        results = []
        for showtime in self._get_showtimes_for_city_date(city, date):
            if genre and showtime.movie.genre != genre:
                continue
            if language and showtime.movie.language != language:
                continue
            available = len(showtime.available_seats)
            if available > 0:
                results.append({
                    'movie': showtime.movie.title,
                    'theater': showtime.theater.name,
                    'time': showtime.start_time.strftime("%H:%M"),
                    'available_seats': available,
                    'showtime_id': showtime.showtime_id,
                    'price_range': self._get_price_range(showtime),
                })
        return sorted(results, key=lambda x: x['time'])

    def _get_price_range(self, showtime: Showtime) -> dict:
        prices = [s.price for s in showtime.auditorium.seats.values()]
        return {'min': min(prices), 'max': max(prices)}

Key Design Decisions

  • All-or-nothing seat hold: Validate all requested seats before holding any. Under the showtime lock, prevents partial holds (user thinks they got seats 5A and 5B but only got 5A).
  • Hold with expiry: Seats held for 10 minutes; is_available() checks expiry lazily (no background cleanup needed for low-traffic cases). High-traffic: background job scans for expired holds.
  • Showtime-level locking: Lock per showtime (not per seat) ensures atomicity of multi-seat operations without deadlock risk. Coarser than per-seat locking but simpler and correct.
  • Payment before confirmation: Seats remain HELD (not BOOKED) until payment succeeds. If payment fails, held seats auto-expire and return to inventory.

Scalability Considerations

  • Database: Optimistic locking for seat status in PostgreSQL: UPDATE seats SET status='held' WHERE seat_id=? AND status='available'; check rows_affected=1
  • Distributed locks: For multi-server deployment, use Redis SETNX for showtime locks instead of in-process threading.Lock
  • Popular shows: Queue users during high-demand ticket sales (Taylor Swift effect); virtual waiting room prevents thundering herd on database
  • Seat map caching: Cache available seat count in Redis; invalidate on each booking/cancellation

{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you handle concurrent seat selection in a movie booking system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use a two-phase approach: (1) Seat hold — atomically mark requested seats as HELD under a showtime-level lock. Validate ALL requested seats are available before holding any (all-or-nothing). The hold has a 10-minute TTL; if payment is not completed, seats auto-expire back to AVAILABLE. (2) Booking confirmation — under the same lock, verify seats are still held by this user and the hold has not expired, then process payment and mark seats BOOKED.”}},{“@type”:”Question”,”name”:”How do you prevent two users from booking the same seat?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Two approaches: (1) In-process locking — use a per-showtime ReentrantLock/RLock; all seat operations for a showtime are serialized. Simple but single-server only. (2) Distributed locking — use Redis SETNX (“set if not exists”) for a showtime lock key; all servers participate. For database-backed systems, use optimistic locking: UPDATE seats SET status=’held’ WHERE seat_id=? AND status=’available’, then check rows_affected=1. If 0 rows updated, another user got the seat first.”}},{“@type”:”Question”,”name”:”How do you implement seat hold expiry in a movie booking system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Two strategies: (1) Lazy expiry — check held_until timestamp inside is_available() on every access. No background job needed; expired holds are cleaned up on first access after expiry. Simple but can show seats as held to browsing users even after expiry. (2) Eager expiry — a background scheduler scans for holds past their expiry timestamp and resets them to AVAILABLE. More accurate seat availability display, but adds operational complexity. Use lazy for low-traffic, eager for high-traffic systems.”}},{“@type”:”Question”,”name”:”How would you scale a movie ticket booking system for high-demand releases?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”For blockbuster releases (Taylor Swift concerts, Star Wars premieres), implement a virtual waiting room: users are queued before the sale opens; the system releases them in batches to prevent database overwhelm. Use Redis counters for real-time seat counts (avoid DB reads for availability checks). Use Redis distributed locks instead of in-process locks. Apply database sharding by movie/showtime to distribute write load. Consider eventual consistency for the seat map display while maintaining strong consistency for the actual booking transaction.”}}]}

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

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

🏢 Asked at: Shopify Interview Guide

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

🏢 Asked at: Lyft Interview Guide 2026: Rideshare Engineering, Real-Time Dispatch, and Safety Systems

🏢 Asked at: Atlassian Interview Guide

Scroll to Top