Design a Movie Ticket Booking System
A movie ticket booking system models physical objects — cinemas, shows, and seats — each with state that must stay consistent across concurrent operations, the hallmark of a real-world-entity problem like a parking lot or a hotel. The defining challenge is seat allocation: at any moment, a seat is either free or booked, and two users trying to book the same seat simultaneously must not both succeed.
See It in Action
The demo below runs the finished allocation logic for one show: tap free seats to select them, then book them. Already-booked seats are rejected — the core invariant the rest of the article builds toward. The grey seats were booked by earlier customers.
Clarifying Requirements
Good first questions narrow the scope to a buildable design.
Does the system manage multiple shows simultaneously? Yes — a cinema runs several shows at once, each in a different screen. Are seats numbered? Yes, each show assigns fixed seat numbers starting at 1. Can a booking be cancelled? Yes. Do we need pricing or payment? No — this design covers availability and allocation only. What happens when a seat is already booked and someone tries to book it again? Return an error message; do not overwrite.
The functional requirements for this walkthrough: a Cinema holds multiple shows identified by a
show ID; each show has a fixed number of seats numbered 1 through N; a show command creates a
show; a book command books a specific seat for a named user, failing if the seat is already
taken or out of range; a cancel command releases a booked seat; an available command reports
how many free seats remain.
The two primary flows — booking and cancellation — start at the caller and each return a single string result.
Core Entities
Scanning the requirements for nouns that carry state yields three objects.
Seat owns the central invariant: it holds at most one booking at a time. Its state is
a seat number (immutable) and the name of the user who booked it (None when free). Its
behavior is is_free, book, and cancel. Nothing outside Seat writes its booking field
directly — the invariant lives here.
Show represents one screening. It owns a collection of seats indexed by seat number and
exposes book, cancel, and available. The show is responsible for validating that a
seat number is in range before delegating to the seat. This keeps range-checking in one place:
Show.book asks "is this seat number valid?" and then tells the seat what to do — it does not
expose the seat map to callers.
Cinema is the facade. It maps show IDs to shows and routes each command to the right show.
It is the only object the command dispatcher talks to; Show and Seat are internal
implementation details. The cinema is also responsible for detecting duplicate show creation.
The prompt revolves around "bookings," so a Booking object looks like an obvious fourth entity.
Before reading on, decide whether a booking earns its own class here, or whether its state belongs
somewhere simpler.
Decision checkpoint
Booking and cancelling are the core operations. Should a Booking be its own value object?
So booking state lives on the Seat — booked_by is the booking — and no separate Booking
class is introduced.
Designing the Classes
Step 3 of the delivery framework derives each class's state from specific requirements, then its behavior as method signatures — before writing any implementation. The principle is Tell, Don't Ask: a class owns decisions about its own data.
Seat
The requirement "a seat is either free or booked by one user" maps to a single nullable field:
booked_by: str | None. That one field drives all three methods. is_free tests whether the
field is None. book(user) writes it. cancel clears it. No caller reads booked_by
directly — callers ask is_free() and receive yes or no.
The seat's number is immutable after construction; it is needed only for building the response
string in the show's book and cancel methods. Keeping it on the seat avoids passing the
number as a separate parameter through the call chain.
Booking a seat involves two distinct checks: that the seat number is in range, and that the seat
is actually free. The Seat owns its own free/booked state. But which class should validate that
the seat number exists at all? Before reading on, decide where the range check belongs.
Decision checkpoint
A book request can fail two ways: the seat number is out of range, or the seat is already taken. Which class checks range, and which checks vacancy?
Show
The requirement "seats are numbered 1 through N" determines the data structure: a dictionary
from integer seat number to Seat. A list would also work, but a dictionary makes the
existence check (seat_num not in self.seats) explicit rather than relying on an index bound.
Three behaviors follow directly from the commands. book(seat_num, user) must check existence,
then check vacancy, then delegate — in that order. cancel(seat_num) checks existence, then
checks that the seat is actually booked, then delegates. available() counts free seats.
The show never exposes its seat dictionary; callers can only ask it questions.
Cinema
The requirement "multiple shows each with a unique ID" gives the state: shows: dict[str, Show].
add_show guards against duplicate IDs. Each of book, cancel, and available first looks up
the show by ID and returns an error string if not found, then forwards the request to the show.
The cinema's public interface matches the four commands exactly. Nothing else is public. A small public surface is easier to test and harder to misuse.
Try It Yourself
Before reading the implementation, build it yourself. Implement run_cinema(instructions) that
replays a command stream and returns one result line per command.
Each instruction is a list of strings. Four commands are supported:
["show", showId, seats]— create a show with the given number of seats (numbered 1 to N); return"Show <showId>: <seats> seats". If the show ID already exists, return"Show <showId> exists".["book", showId, seat, user]— book the seat for the user; return"Booked seat <seat> for <user>". Errors:"Unknown show <showId>","No seat <seat>","Seat <seat> taken".["cancel", showId, seat]— release the seat; return"Cancelled seat <seat>". Errors:"Unknown show <showId>","Seat <seat> not booked".["available", showId]— return"Available: <count>". Error:"Unknown show <showId>".