DEV Community

Cover image for Designing a movie ticket booking system: the low-level design interview, explained
Gowtham Potureddi
Gowtham Potureddi

Posted on

Designing a movie ticket booking system: the low-level design interview, explained

Designing a movie ticket booking system: the low-level design interview, explained

"Design a movie ticket booking system" (think BookMyShow or Fandango) is a staple of the low-level design interview, and it is a trap for anyone who treats it as a data-modelling exercise. The classes are the easy part — cities, cinemas, screens, shows, seats. The reason interviewers love it is the one requirement everything hinges on: two people must never book the same seat, and the moment you admit that thousands of users are staring at the same seat map for a popular show, you are no longer drawing boxes — you are designing for concurrency. What the round is really testing is whether you can turn a familiar product into a clean object model and defend it when the interviewer asks what happens when two users click the last premium seat at the same instant.

This guide walks the booking system the way a strong candidate works it in an LLD / OOD interview: clarify the requirements, identify the core entities, sketch the classes and the key methods, and then defend the handful of decisions the interviewer always pushes on — how a seat gets locked so it cannot be double-booked, what the booking's lifecycle looks like, what happens when a user's hold expires while they are on the payment page, and how pricing stays flexible. In a low-level design round you write these classes out in an editor, not as a finished program but as a design you can explain and extend out loud, one method at a time.

Title card for a low-level design interview explainer on designing a movie ticket booking system, showing a class diagram with a City containing Cinemas, each Cinema with Screens, Shows scheduled on screens, a seat map with available, locked and booked seats, and a Booking linking a user to a show and seats, with tags for seat locking and the State pattern, on a dark citrus-and-lavender background.

Why the movie ticket booking system comes up in low-level design interviews

The booking system earns its place because it hides a genuinely hard problem inside a product everyone has used. There are obvious entities (cinemas, screens, shows, seats, bookings) and obvious relationships (a cinema has screens, a screen hosts shows, a show has a seat map). But the instant you say "a user selects seats and pays," the interviewer has a dozen sharp questions: what stops someone else taking those seats while the first user types in their card? What if they never finish paying? What if two requests for the same seat arrive in the same millisecond? An interviewer can watch you make a clean model and then find out, in one question, whether you have ever thought about a shared resource under contention.

The interviewer rarely lets a tidy seat map stand on its own. "It's a Friday-night blockbuster. There's one premium seat left, and two users tap it at the exact same moment; both of their requests call selectSeat() and both see it as available. Walk me through what your code does so that only one of them ends up with that seat — and then tell me what happens if the user who got it sits on the payment page until their hold expires." That question — atomic locking and lock expiry — is the one a pretty class diagram can never survive, and the one this problem is really testing.

The rounds on Zynter are modelled on how the loop runs, not scraped from any candidate's recall. What matters for prep is the shape: you put a class on the board, and the next sentence out of the interviewer's mouth attacks the seam where two users meet one seat.

What the interviewer is really testing

A low-level design answer is read on the same four dimensions as any design round. Problem solving — did you recognise that seat selection is a concurrency problem and design a lock, or did you model only a single calm user? Communication — can you justify why a temporary seat lock with an expiry beats simply marking a seat "booked" the moment it is clicked? Technical execution — do you make the lock acquisition atomic, handle the expiry, and define the booking's states, or does your happy path quietly double-book? Time management — did you clarify scope before coding and spend your half hour on the locking and lifecycle, not on perfecting the Movie class's getters? Reading this guide teaches the design; it does not teach you to defend it while someone says "now the payment service times out mid-booking."

Step 1: Clarify the requirements

The biggest mistake in an LLD round is writing a class before asking a question. Spend the first two minutes scoping out loud — it is graded.

Functional requirements to confirm:

  • A user can browse cities, then cinemas, then movies and the shows (a movie on a screen at a time).
  • A show runs on a screen with a fixed seat map; seats have types (regular, premium) and per-seat status.
  • A user selects one or more seats, the system holds them, the user pays, and the booking is confirmed with tickets.
  • The core invariant: a seat can be sold to at most one booking for a given show — no double booking, ever.

Non-functional / constraints (where senior candidates separate themselves):

  • Concurrency — many users view and select seats for the same popular show simultaneously; seat selection is the contended resource and must be safe.
  • Lock expiry — a held seat must be released if the user does not pay within a window (say five minutes), so seats are not held hostage by abandoned carts.
  • Extensibility — pricing (by seat type, show time, demand) and seat layouts should vary without rewriting the booking flow.
  • Scope boundaries — are payments, refunds, cancellations and offers in scope? Agree what is in and out, out loud, so you are not marked down for deferring something deliberately.

State your assumptions: "I'll assume payment is a separate service I'll stub, a single confirmed booking per set of seats, and seat holds that expire after five minutes — tell me if you want cancellations in scope." Prioritising out loud is itself part of the score.

Step 2: Identify the core entities

With requirements agreed, name the classes before writing any. The nouns are your first draft of the object model:

  • City → Cinema (a theatre in a city) → Screen (a hall inside a cinema) — a straightforward containment chain.
  • Movie — title, duration, language, certification.
  • Show — a Movie on a Screen at a start time; owns the per-show seat status (which seats are available, locked or booked for this show).
  • Seat — a physical seat in a screen with a row/number and a SeatType (REGULAR, PREMIUM); its bookable status is tracked per show, not on the seat itself.
  • SeatLock and SeatLockManager — a temporary hold on seats for a show, with the user who holds it and an expiry timestamp; the manager grants and releases locks atomically.
  • Booking — links a User to a Show and a set of seats, and carries a status through its lifecycle.
  • Payment — a stubbed transaction that drives a booking from pending to confirmed.
  • BookingService — the orchestrator the panels call: lock seats, create a booking, take payment, confirm.

Naming the relationships out loud matters: a City has many Cinemas, a Cinema has many Screens, a Screen hosts many Shows, a Show has a seat map, and a Booking references one show and its seats. That "has-a" vocabulary is what the interviewer listens for — and note the subtle call to keep seat status on the Show, not the Seat, because the same physical seat is available for one show and sold for the next.

A class diagram for a movie ticket booking low-level design, showing a City composed of Cinema objects, each Cinema with Screen objects, Screens hosting Show objects that reference a Movie, a Seat with a SeatType enum whose bookable status is tracked per Show, a SeatLockManager granting temporary SeatLocks with an expiry, and a Booking linking a User to a Show and its seats with a status enum, drawn with composition and association arrows.

Step 3: Sketch the classes

Now write the classes — as skeletons you can talk through, not a finished program. Lead with the enums and the lock manager, because they carry the design decisions.

enum SeatType { REGULAR, PREMIUM }
enum SeatStatus { AVAILABLE, LOCKED, BOOKED }
enum BookingStatus { CREATED, SEATS_LOCKED, PAYMENT_PENDING, CONFIRMED, EXPIRED, CANCELLED }

class Seat {
    private String id;      // e.g. "A12"
    private SeatType type;
    // physical seat; bookable status lives on the Show, keyed by seat id
}

class SeatLock {
    private Show show;
    private List<Seat> seats;
    private String heldByUserId;
    private Instant expiresAt;   // e.g. now + 5 minutes
    public boolean isExpired() { return Instant.now().isAfter(expiresAt); }
}

class SeatLockManager {
    // one lock entry per (show, seat); guarded so acquisition is atomic
    private final Map<String, SeatLock> locks = new ConcurrentHashMap<>();

    public synchronized boolean lockSeats(Show show, List<Seat> seats, String userId) {
        for (Seat s : seats) {                       // all-or-nothing
            SeatLock existing = locks.get(key(show, s));
            if (existing != null && !existing.isExpired()) return false; // someone holds it
        }
        SeatLock lock = new SeatLock(show, seats, userId, Instant.now().plusSeconds(300));
        for (Seat s : seats) locks.put(key(show, s), lock);
        return true;                                 // caller won the seats
    }
    public synchronized void unlock(Show show, List<Seat> seats, String userId) { /* release if held by user */ }
}

class BookingService {
    private SeatLockManager lockManager;

    public Booking createBooking(User user, Show show, List<Seat> seats) {
        if (!lockManager.lockSeats(show, seats, user.getId()))
            throw new SeatsUnavailableException();   // lost the race
        return new Booking(user, show, seats, BookingStatus.SEATS_LOCKED);
    }

    public void confirm(Booking booking, Payment payment) {
        if (booking.locksExpired()) { booking.setStatus(BookingStatus.EXPIRED); throw new LockExpiredException(); }
        payment.process();                           // on success:
        booking.markSeatsBooked();                   // AVAILABLE/LOCKED -> BOOKED
        booking.setStatus(BookingStatus.CONFIRMED);
    }
}
Enter fullscreen mode Exit fullscreen mode

Talk through the choices as you write them: the SeatLockManager is the heart — lockSeats is synchronized (or uses an atomic per-seat compare-and-set) and is all-or-nothing, so two users cannot both acquire the same seat; the lock carries an expiresAt, so an abandoned cart frees the seats automatically; and the Booking carries a status enum, which sets up the state-machine conversation.

Step 4: The design decisions interviewers probe

The diagram is the easy half. The follow-ups live in a handful of decisions, and having an answer ready for each is what the round rewards.

  • Atomic seat locking — the race for the last seat — this is the follow-up from the opening scenario and the one most candidates fumble. Two requests read a seat as AVAILABLE and both try to take it. The fix: lock acquisition must be an atomic check-and-set — a synchronized section in the SeatLockManager, a per-seat lock, or a compare-and-set on the seat's status — so exactly one caller wins and the other gets "seats unavailable" and re-picks. State the insight plainly: the seat's status is shared mutable state, so "is it free? then take it" must be one indivisible step, or two bookings capture one seat. In a distributed setting you would push this to the database (a conditional update / SELECT … FOR UPDATE) or a distributed lock, and saying so shows range.
  • Lock expiry mid-payment — the canonical booking follow-up: the user locks seats, goes to pay, and the hold expires while they are typing. The design answer: the lock has a TTL; when confirm runs it re-checks the lock is still valid and held by this user before taking payment — if it expired, the booking moves to EXPIRED and the seats are already free for others; and you never debit the card for an expired hold. A background sweeper (or lazy check on next access) releases expired locks. Naming "check the lock is still mine at payment time" is the detail interviewers want.
  • Booking state machine — model the lifecycle explicitly: CREATED → SEATS_LOCKED → PAYMENT_PENDING → CONFIRMED, with EXPIRED (lock timed out) and CANCELLED (user or payment failure) as terminal branches. A State pattern or a guarded status enum keeps illegal transitions (confirming an expired booking) impossible.
  • Pricing — keep the fee out of Booking; use a Strategy so price can vary by seat type, show time or demand (a premium seat on a Friday night costs more) by adding a class, not editing the booking flow.
  • Edge cases — payment failure after locking (release the lock, mark CANCELLED), a user holding more seats than allowed, a show that sells out, and a double-submit of the pay button (idempotency on the booking). Naming these shows you see past the happy path.

A diagram of the movie ticket booking design decisions, showing two users racing for the same last seat where an atomic check-and-set in the seat lock manager lets only one win while the other is told the seat is unavailable, a seat lock with a five-minute expiry that frees the seats if payment is not completed, and the booking state machine moving from created to seats-locked to payment-pending to confirmed, with expired and cancelled as terminal states.

Low-Level Design
LLD · Ticket Booking
Design the booking system and defend the race for the last seat out loud

Practise on Zynter →

Design patterns that fit the booking system

You do not need to force patterns in, but naming the ones that fit — and why — is a strong signal in an OOD round.

  • State — the booking lifecycle. A State interface (or a guarded status enum) makes each transition explicit and illegal ones impossible — you cannot confirm an expired booking. The standout pattern for this problem.
  • Strategy — pricing. A PricingStrategy lets the fare vary by seat type, time and demand independently of the booking flow.
  • Singleton — the SeatLockManager and BookingService are typically single shared instances so all requests contend on one authoritative lock table; note the singleton must be thread-safe.
  • Observer — notifications: a confirmed booking notifies the user (ticket, email) and could update a live seat-availability view, without the booking logic knowing about those channels.

Say it crisply: State for the booking lifecycle and the atomic lock for correctness are the two that win this round; Strategy keeps pricing flexible; the lock manager is a thread-safe singleton. Naming the trade-off, not just the pattern, is what reads as experience.

A diagram mapping design patterns to the movie ticket booking system, showing the State pattern governing the booking lifecycle, a Strategy for pricing by seat type and demand, a Singleton thread-safe SeatLockManager holding the authoritative lock table, and an Observer notifying the user and the seat-availability view when a booking is confirmed.

Low-Level Design
LLD · Core skills
Apply State, Strategy and atomic locking out loud

Practise on Zynter →

How to talk about a low-level design in an interview

The probes from every section rhyme. Master these five and you can defend any LLD, not just the booking system.

  • "What are the core classes here?" → Name the nouns — City, Cinema, Screen, Show, Movie, Seat, Booking, SeatLockManager — and the relationships (has-a, is-a), and note that seat status lives on the Show, not the Seat. (communication)
  • "Two users click the last seat at once — what happens?" → Lock acquisition is an atomic check-and-set in the SeatLockManager, so exactly one wins and the other is told the seats are unavailable; the seat status is shared mutable state, so the check and the set are one indivisible step. (technical execution)
  • "The hold expires while the user is paying — then what?" → The lock has a TTL; confirm re-checks the lock is still valid and held by this user before charging, so an expired hold moves the booking to EXPIRED, frees the seats, and never debits the card. (problem solving)
  • "Walk me through the booking's lifecycle." → CREATED → SEATS_LOCKED → PAYMENT_PENDING → CONFIRMED, with EXPIRED and CANCELLED as terminal states; a State pattern or guarded enum makes illegal transitions impossible. (communication)
  • "Where does pricing live?" → Behind a PricingStrategy, so a premium Friday-night fare is a new class, not an edit to Booking — open/closed in action. (problem solving)

Each maps to a debrief dimension. Reading them teaches the answers; it does not teach you to deliver them while someone says "now the payment service times out after you locked the seats" — that only comes from saying them out loud.

How to prepare: a short plan

You do not need to memorise one reference solution. You need the OOD process automatic, so any "design X" prompt becomes requirements → entities → classes → decisions.

  • Day 1: Drill the process on the booking system — clarify, list entities, sketch classes with relationships, and get the containment chain (City → Cinema → Screen → Show) and the seat map on the board in ten minutes.
  • Day 2: Drill the decisions — atomic seat locking, lock expiry mid-payment, the booking state machine, and pricing via Strategy. These are where the follow-ups live, and the concurrency answer is the one that wins the round.
  • Day 3: Do it out loud, timed, with someone pushing on the race for the last seat and the expired hold, then move to a sibling LLD (parking lot, elevator) to prove the process transfers.

This explainer feeds the broader low-level design interview track — the booking system is the LLD problem where concurrency finally bites, and the locking process it teaches transfers to any design with a contended resource.

Frequently asked questions

What is a movie ticket booking system interview actually testing?

It is testing object-oriented design under a concurrency constraint, not data modelling for its own sake. The entities — cities, cinemas, screens, shows, seats, bookings — are straightforward, so the interviewer uses them as a setup for the real question: how do you guarantee that a seat is sold to at most one booking when thousands of users are selecting seats for the same popular show at the same time? A strong answer produces a clean class model, keeps seat status on the show rather than the physical seat, and then designs an atomic seat lock with an expiry and an explicit booking lifecycle. The round rewards recognising the shared-resource problem and defending the locking and state design out loud, including the unhappy paths like an expired hold and a failed payment.

How do you prevent two users from booking the same seat?

You make seat acquisition atomic. The problem is that two requests can both read a seat as available and both try to take it, so the check ("is this seat free?") and the set ("mark it mine") must happen as one indivisible operation. In a single-process design this is a synchronized section or a per-seat lock in a SeatLockManager, or a compare-and-set on the seat's status, so exactly one caller succeeds and the other is told the seats are unavailable and must pick again. In a distributed system you push the same idea to the database with a conditional update or SELECT … FOR UPDATE, or use a distributed lock. The key sentence is that the seat's status is shared mutable state, so you cannot check and set it in two separate steps without risking a double booking.

What is seat locking and why does it expire?

Seat locking is a temporary, exclusive hold the system places on the seats a user has selected, so that while that user proceeds to payment, no one else can take those seats. The lock is granted atomically by a SeatLockManager and carries the ID of the user who holds it and an expiry timestamp, typically a few minutes out. It expires because otherwise a user who selects seats and then abandons the flow — closes the tab, never pays — would hold those seats hostage forever, and on a popular show that would strand inventory. When the lock expires, the seats return to available for other users; the booking's confirm step always re-checks that the lock is still valid and still held by that user before taking payment, so an expired hold can never result in a charge or a confirmed seat.

What happens if the lock expires while the user is paying?

The design must handle this explicitly, because it is the most common follow-up. The seat lock has a time-to-live, and the booking's confirmation step re-validates the lock before it processes payment: if the lock has expired, the booking transitions to an EXPIRED state, the seats are already (or are then) released for other users, and crucially the payment is not taken. If payment was somehow already initiated, you either refund it or, better, you only ever charge after confirming the lock is still held, so the window for a charge against freed seats is closed. A background sweeper or a lazy check on the next access cleans up expired locks. The sentence interviewers want is "I re-check that the lock is still mine at payment time," which shows you thought about the race between the expiry timer and the payment.

Which design patterns are used in a movie ticket booking system?

The standout is the State pattern for the booking lifecycle: a booking moves through CREATED, SEATS_LOCKED, PAYMENT_PENDING and CONFIRMED, with EXPIRED and CANCELLED as terminal states, and modelling these as states (or a guarded status enum) makes illegal transitions — like confirming an expired booking — impossible. The Strategy pattern fits pricing, letting the fare vary by seat type, show time and demand without changing the booking flow. The SeatLockManager and BookingService are usually Singletons so all requests contend on one authoritative, thread-safe lock table. An Observer pattern fits notifications and a live seat-availability view that react when a booking is confirmed. As always in an OOD round, the point is to name the pattern and the trade-off, not to force patterns in where they add nothing.

How do I practise a low-level design interview out loud?

Reading a booking-system solution is not the same as defending your seat lock while an interviewer makes two users race for the last seat or expires a hold mid-payment. On Zynter, the LLD track runs live voice rounds modelled on how real low-level design loops run — you write your classes and methods in the editor and a voice interviewer follows up one question at a time ("two users just tapped the same seat — what does your code do?", "the hold expired while they were paying — now what?"), then gives you a debrief on problem solving, communication, technical execution and time management. The score is a practice signal, not a hiring decision.

Practise it on Zynter

You can draw the seat map. The round is won on defending the race for the last seat out loud.

Pick the Ticket Booking low-level design round, write your classes in the editor, and defend them out loud to a voice interviewer that keeps asking "two users just tapped the last seat — what does your code do?" and "the hold expired while they were on the payment page — what happens to the seats and the charge?" — then read the debrief. Zynter.ai is on-demand mock interviews modelled on real company loops, with 150+ rounds across 20 companies including the low-level design track (Sep 2026).

Practise the Ticket Booking LLD round →
Browse all interview tracks

Top comments (0)