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

Source: https://www.techinterview.org/post/3233463752/lld-movie-ticket-booking-system/
Updated: 2026-04-17 · techinterview.org

## 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

🏢 Asked at: [Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering](https://techinterview.org/uber-interview-guide/)

🏢 Asked at: [Airbnb Interview Guide 2026: Search Systems, Trust and Safety, and Full-Stack Engineering](https://techinterview.org/airbnb-interview-guide/)

🏢 Asked at: [Shopify Interview Guide](https://techinterview.org/shopify-interview-guide/)

🏢 Asked at: [Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems](https://techinterview.org/stripe-interview-guide/)

🏢 Asked at: [Lyft Interview Guide 2026: Rideshare Engineering, Real-Time Dispatch, and Safety Systems](https://techinterview.org/lyft-interview-guide/)

🏢 Asked at: [Atlassian Interview Guide](https://techinterview.org/atlassian-interview-guide/)
