Design a Movie Ticket Booking System
You'll learn to
- -Model theaters, shows, seats, and bookings, and prevent two users from ever booking the same seat concurrently
- -Use State to represent seat lifecycle (available, locked, booked) with a time-boxed hold before payment
This prompt combines the concurrency-sensitive resource assignment from the Parking Lot case study with a genuinely new wrinkle: a seat needs to be temporarily held during the payment flow, not just instantly claimed - if it were instant-claim like a parking spot, a user who abandons checkout would permanently lock a seat no one else could ever book.
Step 1: Core Entities
class Seat:
def __init__(self, seat_id: str):
self.seat_id = seat_id
self.state: "SeatState" = AvailableState()
class Show:
def __init__(self, show_id: str, movie: str, seats: list[Seat]):
self.show_id = show_id
self.movie = movie
self.seats = {s.seat_id: s for s in seats}Step 2: Seat Lifecycle as State
A seat moves through Available -> Locked (held during checkout, with an expiry) -> Booked (payment confirmed), or back to Available if the hold expires or checkout is cancelled - exactly the state-machine shape from the State pattern chapter, with the added wrinkle of a time-based automatic transition.
import time
class SeatState(ABC):
@abstractmethod
def lock(self, seat: Seat, user_id: str) -> bool: ...
@abstractmethod
def confirm(self, seat: Seat) -> bool: ...
@abstractmethod
def release(self, seat: Seat) -> None: ...
class AvailableState(SeatState):
def lock(self, seat: Seat, user_id: str) -> bool:
seat.state = LockedState(user_id, expires_at=time.time() + 300) # 5-minute hold
return True
def confirm(self, seat: Seat) -> bool: return False
def release(self, seat: Seat) -> None: pass
class LockedState(SeatState):
def __init__(self, user_id: str, expires_at: float):
self.user_id = user_id
self.expires_at = expires_at
def _check_expiry(self, seat: Seat) -> None:
if time.time() > self.expires_at:
seat.state = AvailableState()
def lock(self, seat: Seat, user_id: str) -> bool:
self._check_expiry(seat)
return False # already locked (by this or another user)
def confirm(self, seat: Seat) -> bool:
self._check_expiry(seat)
if isinstance(seat.state, LockedState):
seat.state = BookedState()
return True
return False # lock expired before payment completed
def release(self, seat: Seat) -> None:
seat.state = AvailableState()
class BookedState(SeatState):
def lock(self, seat: Seat, user_id: str) -> bool: return False
def confirm(self, seat: Seat) -> bool: return False
def release(self, seat: Seat) -> None: pass # a real system: only via a cancellation flowStep 3: Concurrency-Safe Locking
Just like the Parking Lot's atomic search-and-assign, locking a seat must be a single atomic step guarded by a lock - two users clicking the same seat simultaneously must never both succeed. The BookingService wraps the lock() call in a per-seat (or per-show) lock, exactly the pattern established in the Concurrency module.
import threading
class BookingService:
def __init__(self):
self._lock = threading.Lock()
def hold_seat(self, show: Show, seat_id: str, user_id: str) -> bool:
with self._lock:
seat = show.seats[seat_id]
return seat.state.lock(seat, user_id)
def confirm_booking(self, show: Show, seat_id: str) -> bool:
with self._lock:
seat = show.seats[seat_id]
return seat.state.confirm(seat)Step 4: Expiring Abandoned Holds
The design above checks expiry lazily (only when lock() or confirm() is next called on that seat) rather than running a background timer per seat, which is a deliberate simplicity trade-off worth naming: it means a seat whose hold has technically expired might still show as "locked" to a status-check call until someone actually tries to interact with it, which is an acceptable trade-off for a first-pass design, with a background sweep as the natural follow-up if the interviewer pushes on it.
A subtle bug to watch for: LockedState.confirm() must re-check expiry before confirming, not just before locking - otherwise a payment that completes microseconds after the hold expired would incorrectly succeed on an already-released seat.
Interview Signal is part of Pro
See a real weak answer next to a real strong one for this exact topic.
Quiz is part of Pro
Test what you just read with a short quiz, and bank the XP.