Design Projects: Classic Systems · Movie Ticket Booking

Design a Movie Ticket Booking System

Designing a ticket-booking service: per-show seat state, expiring holds, and concurrency control.

  • 8 min read
  • 16 practice questions
  • 4 graded problems
On this page
  1. Description
  2. The approach
  3. The pieces in code
  4. Concurrency
  5. Where you'll see it
  6. Which tool for which job?
  7. Common mistakes
  8. Check yourself
  9. Key takeaways
  10. Quick revise
  11. Practice

Description

A movie booking system is the classic "two people, one seat" problem: seats are scarce, many users browse the same show at once, payment takes time, and the design must never sell a seat twice or strand one that nobody can buy. This skill models availability per show, uses timed holds between choosing seats and paying, and makes booking all-or-nothing under concurrency.

A first ticket-booking design usually hangs availability on the seat and books in one step:

Java

class Seat {
    boolean booked;
}

boolean book(List<Seat> seats) {
    for (Seat s : seats) {
        if (s.booked) return false;
        s.booked = true;                 // marks as it goes
    }
    return true;
}

Python

class Seat:
    def __init__(self):
        self.booked = False


def book(seats: list[Seat]) -> bool:
    for s in seats:
        if s.booked:
            return False
        s.booked = True                  # marks as it goes
    return True

Every part of that fails under real use. The same physical seat is sold for every show - its booked flag can't belong to the seat. A group of four fails on the third seat after marking the first two, and those two stay taken. Nobody has paid yet, so either seats are "booked" by people who then abandon checkout, or two people pay for the same seat. And two users clicking at the same instant both read booked == false.

We'll go from simple to more technical:

  1. The model: shows, seats per show, and timed holds.
  2. Holding and confirming seats, with lazy expiry.
  3. Concurrency: one lock per show, or ordered per-seat locks.
  4. The bugs that double-sell or strand seats, and the tradeoffs in hold length.

The approach

  1. Clarify. Shows, screens and layouts; seat types and prices; how long a seat is held during payment; whether a multi-seat selection may be partially filled (almost never); cancellations; one server or many?
  2. Model. Movie and Show (a movie on a screen at a time) - availability belongs to the show. Seat is a value (row, number). A Hold (seats, owner, expiresAt) and a Booking (seats, status). A BookingService owns them and exposes SeatStatus (AVAILABLE, HELD, BOOKED).
  3. API. holdSeats(show, user, seats) returns a hold id or throws SeatUnavailableException; confirm(holdId) returns a booking id; releaseHold, cancelBooking, statusOf, availableSeats.
  4. Core. Per show, seat → hold and seat → booking maps. Holds expire by comparing an injected Clock with expiresAt. Every operation on a show runs under that show's lock, and a hold checks every seat before taking any.

The pieces in code

Java

public synchronized String holdSeats(String showId, String userId, List<Seat> seats) {
    Show show = show(showId);                                   // unknown show -> IllegalArgumentException
    for (Seat seat : seats) requireSeat(show, seat);
    for (Seat seat : seats)                                      // check every seat first...
        if (statusIn(show, seat) != SeatStatus.AVAILABLE) throw new SeatUnavailableException(seat + " is taken");
    Hold hold = new Hold(newId(), showId, List.copyOf(seats), clock.instant().plus(holdTime));
    for (Seat seat : seats) show.heldBy.put(seat, hold.id);     // ...then take them all
    holds.put(hold.id, hold);
    return hold.id;
}

private SeatStatus statusIn(Show show, Seat seat) {
    if (show.bookedBy.containsKey(seat)) return SeatStatus.BOOKED;
    Hold hold = holds.get(show.heldBy.get(seat));
    boolean live = hold != null && !hold.finished && clock.instant().isBefore(hold.expiresAt);
    return live ? SeatStatus.HELD : SeatStatus.AVAILABLE;       // an expired hold simply reads as free
}

Python

def hold_seats(self, show_id: str, user_id: str, seats: list[Seat]) -> str:
    with self._lock:
        show = self._show(show_id)                              # unknown show -> ValueError
        for seat in seats:
            self._require_seat(show, seat)
        for seat in seats:                                      # check every seat first...
            if self._status_in(show, seat) is not SeatStatus.AVAILABLE:
                raise SeatUnavailableError(f"{seat} is taken")
        hold = Hold(self._new_id(), show_id, tuple(seats), self._now() + self._hold_time)
        for seat in seats:
            show.held_by[seat] = hold.id                        # ...then take them all
        self._holds[hold.id] = hold
        return hold.id

def _status_in(self, show: Show, seat: Seat) -> SeatStatus:
    if seat in show.booked_by:
        return SeatStatus.BOOKED
    hold = self._holds.get(show.held_by.get(seat))
    live = hold is not None and not hold.finished and self._now() < hold.expires_at
    return SeatStatus.HELD if live else SeatStatus.AVAILABLE   # an expired hold simply reads as free
  • All or nothing. The check loop runs to completion before the first put, so a failed hold takes no seats.
  • Lazy expiry. Nothing has to delete expired holds for the answer to be right: a hold past expiresAt reads as available, and the next holdSeats overwrites it. A periodic sweep is only housekeeping.
  • The clock is injected. Tests move a fake clock forward five minutes instead of waiting five minutes; the boundary ("held while now is before expiresAt") is written once.
  • confirm refuses an expired or already-used hold, then moves the seats from held to booked. releaseHold removes only the seats still mapped to this hold - heldBy.remove(seat, holdId); in Python, if held_by.get(seat) == hold_id: del held_by[seat] - because an expired hold's seats may already belong to someone else.

Concurrency

"Is it free? Then take it" is a check-then-act: two threads can both see the seat free before either takes it. Within one process the simplest correct answer is a lock per show - here synchronized on the service, or a ReentrantLock per show when shows should proceed in parallel. (In Python, a threading.Lock held with with self._lock:, one per show for parallel shows.) Holding is a few map operations, so contention is short.

Per-seat locks allow more parallelism but invite deadlock: if one request locks A then B while another locks B then A, each waits forever. Sorting the seats and always locking in that order removes the cycle.

Once there are several application servers - or several worker processes of one Python app - an in-process lock protects nothing. The source of truth moves to the database: a unique constraint on (show_id, seat) for bookings, or a conditional update - UPDATE show_seat SET status = 'HELD', hold_id = ? WHERE show_id = ? AND seat = ? AND status = 'AVAILABLE' - and checking that every row was updated, inside one transaction.

Where you'll see it

Real ticketing systems keep holds in a fast store with a time-to-live (for example a Redis key per seat, set only if absent, with an expiry), confirm bookings in a relational database under constraints, and treat the payment gateway's callback as idempotent - it can arrive twice, and the second must not create a second booking. For very high-demand releases they put a virtual waiting room in front, admitting users in batches so the seat map isn't hammered by everyone at once.

Which tool for which job?

You needUse
Availabilityper show, not on the seat
Time to pay without losing the seata hold with an expiry time
Expired holdslazy expiry: treat a hold as gone once its time has passed
Many users on one showone lock per show, or per-seat locks taken in a fixed order
Many serversthe database (unique constraint, conditional update) or a shared store with TTLs
Time in testsan injected clock

Common mistakes

MistakeWhat happensDo this instead
Checking availability, then booking, without a locktwo users book the same seatcheck and take atomically
Taking seats one by one as they're checkeda partial hold stays when a later seat failscheck all, then take all
A release that ignores who holds the seata stale release frees someone else's new holdrelease only your own hold
Holds without expiryseats stuck forever when users leavetimed holds, expired lazily
Per-seat locks in request ordertwo requests deadlocklock seats in a fixed (sorted) order

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. A family of four selects seats; the third one was just held by someone else. What must the system do, and what bug makes it do otherwise?

Show answer

Reject the whole request and take none of the four seats. The bug is checking and taking in the same loop: the first two seats are held before the third fails, and they stay held by a hold nobody knows about. Check every seat first, then take them all - under the show's lock.

2. Ana's hold on seat D4 expired, Ben then held D4, and finally Ana's abandoned browser tab calls releaseHold. What should happen to D4?

Show answer

Nothing - it's Ben's now. A release must only free seats still mapped to the releasing hold (heldBy.remove(seat, holdId)); removing the seat unconditionally would put Ben's seat back on sale while he's paying for it.

3. Why doesn't synchronized protect seats once the service runs on three servers, and what does?

Show answer

Each server has its own JVM and its own lock, so two servers can each hold the same seat at the same time. The shared database has to enforce it: a unique constraint on (show_id, seat), or a conditional update that only changes rows still AVAILABLE, checked inside one transaction (or an atomic set-if-absent with expiry in a shared cache).

Key takeaways

  • Availability belongs to the show, not the seat; Seat is a value.
  • Book in two steps: an expiring hold while the user pays, then a confirmation. Expiry is checked against an injected clock when reading.
  • A hold is all or nothing: check every seat, then take them all, under one lock per show.
  • Release and confirm only what a hold still owns, and refuse expired or used holds.
  • Per-seat locks need a fixed order; several servers need the database (constraints or conditional updates) to enforce one seat, one buyer.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

You needUse
Availabilityper show
Time to paya timed hold, expired lazily
One server, many usersone lock per show (or sorted per-seat locks)
Many serversa unique constraint on (show, seat); holds in the DB or Redis
Late paymentsre-check on confirmation; refund if the seat is gone
Premieresa virtual waiting room

Snippets worth knowing by heart

Java

public synchronized boolean hold(String user, List<String> seats, Instant now) {
    for (String s : seats) {                                   // check everything first
        Hold h = holds.get(s);
        if (booked.contains(s) || (h != null && h.expiresAt().isAfter(now) && !h.user().equals(user))) return false;
    }
    Instant until = now.plus(Duration.ofMinutes(10));
    for (String s : seats) holds.put(s, new Hold(user, until)); // then take everything
    return true;
}

Python

def hold(self, user: str, seats: list[str], now: datetime) -> bool:
    with self._lock:
        for s in seats:                                        # check everything first
            h = self._holds.get(s)
            if s in self._booked or (h and h.expires_at > now and h.user != user):
                return False
        until = now + timedelta(minutes=10)
        for s in seats:                                        # then take everything
            self._holds[s] = Hold(user, until)
        return True

The traps

  • Check-then-book without a lock.
  • Partial holds.
  • Releasing someone else's hold.
  • Holds that never expire.
  • Per-seat locks in request order.

Staff-level points, one line each

  • Pessimistic holds for contention; optimistic version checks when conflicts are rare.
  • A unique constraint on (show_id, seat) rejected the second booking - the cross-server guarantee.
  • Holds in Redis with SET NX PX, or DB rows with held_until and SKIP LOCKED.
  • Late payment webhooks: re-check, refund automatically, process idempotently.
  • Seat selection avoids single-seat gaps (xx......xx: take 2-3, not 3-4).
  • Waiting rooms admit shoppers at a controlled rate.
  • Store the price and layout version a booking was sold with.

Say it in an interview

  • "Seats are held for ten minutes while you pay; expired holds are just ignored."
  • "Booking checks every seat first, then takes them all, under the show's lock."
  • "Across servers, the database's unique constraint is the final guarantee."
  • "If payment lands after the hold expired and the seat's gone, we refund automatically."

Practice

This skill comes with 16 multiple-choice questions that tell you which misconception a wrong answer reveals and 4 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Design a Movie Ticket Booking System free

Find the Best Block of Seats

MediumProCoding problemJava · Python

A cinema hands out the best block of k adjacent seats for each request. The hall is a grid: . is a free seat and X a taken one, and row 0 is the best row.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Build a Movie Ticket Booking Service

HardProDesign projectJava · Python

Build the seat-booking core of a cinema app. A user picks seats for a show and gets a hold while they pay; confirming the hold turns it into a booking.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Debug: Movie Booking - Double-Sold Seats

HardProDebugging taskJava · Python

At a premiere's on-sale moment two customers were both sold the same two seats. The cinema's report, the booking API's log and the last change to the service are in the project's README.md.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Review: Movie Booking - An AI-Suggested Speed-Up

HardProCode reviewJava · Python

Seat selection was slow at on-sale moments, so a teammate asked an AI assistant to "make holdSeats faster under load" and opened a pull request with its version of the service.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Design a Movie Ticket Booking System free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime