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