Skip to content

Movie Ticket Booking (LLD)#

Problem statement (interviewer prompt)

Design the seat-booking domain for a cinema / multiplex (BookMyShow-LLD-style). Model cinemas, screens, shows, seat categories, seat-hold-then-pay flow, payments, ticket generation, and refunds - focusing on the OO classes + state machine.

classDiagram
  class Theater
  class Hall
  class Show
  class Seat
  class Booking
  class Payment
  Theater "1" *-- "many" Hall
  Hall "1" *-- "many" Show
  Show "1" *-- "many" Seat
  Show "1" -- "many" Booking
  Booking --> Payment
classDiagram
  direction TB

  class Theater {
    +id
    +name
    +city
    +halls: List~Hall~
  }
  class Hall {
    +id
    +name
    +rows
    +shows: List~Show~
  }
  class Movie {
    +id
    +title
    +duration
    +genre
    +lang
    +rating
  }
  class Show {
    +id
    +movie: Movie
    +start
    +end
    +hall: Hall
    +seats: List~ShowSeat~
    +pricing: PricingPolicy
  }
  class Seat {
    +id
    +row
    +col
    +type
  }
  class ShowSeat {
    +show: Show
    +seat: Seat
    +state: SeatState
    +heldBy
    +heldUntil
  }
  class SeatState {
    <<enumeration>>
    AVAILABLE
    HELD
    BOOKED
  }

  class User
  class Booking {
    +id
    +user: User
    +show: Show
    +seats: List~ShowSeat~
    +totals
    +status: BookingState
    +idempotencyKey
  }
  class BookingState {
    <<enumeration>>
    PENDING
    PAYMENT_INIT
    CONFIRMED
    CANCELLED
    FAILED
  }
  class PaymentService
  class PricingPolicy {
    <<interface>>
    +price(seat, show) Money
  }
  class HoldService {
    +hold(seats, user, ttl)
    +release(seats)
  }
  class BookingSaga

  Theater *-- Hall
  Hall *-- Show
  Show *-- "many" Seat
  Show *-- "many" ShowSeat
  Show --> PricingPolicy
  User -- Booking
  Booking *-- "many" ShowSeat
  Booking --> BookingState
  ShowSeat --> SeatState
  BookingSaga ..> HoldService
  BookingSaga ..> PaymentService

Detailed class model with multiplicities#

The expanded model below names the actors that show up in a real cinema chain: BookingService orchestrates the booking flow; Cinema owns many Auditoriums (also called screens or halls); each Auditorium runs a stream of Shows through the day; each Show materialises one Seat row per physical seat with a category attached. The seat categories are encoded as a SeatType enum that drives both pricing and the seat-map UI. Booking carries its state machine inline; Payment is a separate aggregate so the gateway lifecycle does not couple to the booking lifecycle.

classDiagram
  direction TB
  class BookingService {
    +selectSeats(showId, seatIds, customerId) HoldToken
    +confirmBooking(holdToken, paymentInfo) Booking
    +cancelBooking(bookingId)
  }
  class Cinema {
    +id
    +name
    +city
    +auditoriums: List~Auditorium~
  }
  class Auditorium {
    +id
    +name
    +rows
    +cols
    +format
  }
  class Show {
    +id
    +movie
    +auditorium: Auditorium
    +startTime
    +endTime
    +pricingStrategy: PricingStrategy
  }
  class Seat {
    +id
    +row
    +col
    +type: SeatType
    +basePrice: Money
  }
  class SeatType {
    <<enumeration>>
    REGULAR
    PREMIUM
    RECLINER
  }
  class Booking {
    +id
    +customer: Customer
    +show: Show
    +seats: List~Seat~
    +total: Money
    +state: BookingState
    +idempotencyKey
  }
  class BookingState {
    <<enumeration>>
    HELD
    CONFIRMED
    CHECKED_IN
    CANCELLED
    REFUNDED
  }
  class Payment {
    +id
    +amount: Money
    +method
    +status
    +authorizedAt
  }
  class Customer {
    +id
    +name
    +email
    +phone
  }
  class PricingStrategy {
    <<interface>>
    +price(show, seats) Money
  }
  class Notifier {
    <<interface>>
    +notify(event)
  }

  BookingService ..> Booking
  BookingService ..> Payment
  Cinema "1" *-- "many" Auditorium
  Auditorium "1" *-- "many" Show
  Show "1" *-- "many" Seat
  Show "1" --> "1" PricingStrategy
  Booking "many" --> "1" Customer
  Booking "many" --> "1" Show
  Booking "1" *-- "many" Seat
  Booking "1" --> "1" Payment
  Booking --> BookingState
  Seat --> SeatType
  BookingService ..> Notifier

Multiplicities make the inventory math explicit. One Cinema owns many Auditoriums. Each Auditorium schedules many Shows. Each Show materialises its own row per physical seat (a Seat row per show, not a shared row across shows) so that holds and bookings on the 7pm show do not collide with the 10pm show in the same hall. A Booking belongs to exactly one Customer and one Show, references many Seats, and points at exactly one Payment outcome.

Seat locking flow in Java#

The seat-locking flow has three operations on the critical path: selectSeats acquires a short TTL distributed lock for the chosen seats, confirmBooking validates the lock is still held, takes payment, persists the booking, and finally releases the lock. The Redis SETNX per seat key gives us atomic "first writer wins" semantics across replicas; the 8-minute TTL guarantees abandoned carts never wedge inventory.

public class BookingService {
    private final SeatLockManager locks;          // Redis-backed SETNX + TTL
    private final BookingRepository bookings;
    private final PaymentGateway payments;
    private final EventBus bus;
    private static final Duration HOLD_TTL = Duration.ofMinutes(8);

    public HoldToken selectSeats(long showId, List<Long> seatIds, long customerId) {
        String token = UUID.randomUUID().toString();
        List<Long> acquired = new ArrayList<>();
        for (Long seatId : seatIds) {
            String key = "lock:show:" + showId + ":seat:" + seatId;
            if (locks.tryAcquire(key, token, HOLD_TTL)) {
                acquired.add(seatId);
            } else {
                acquired.forEach(s -> locks.release("lock:show:" + showId + ":seat:" + s, token));
                throw new SeatUnavailable(seatId);
            }
        }
        return new HoldToken(token, showId, seatIds, customerId, Instant.now().plus(HOLD_TTL));
    }

    @Transactional
    public Booking confirmBooking(HoldToken hold, PaymentRequest pr) {
        if (!locks.validateAll(hold.lockKeys(), hold.token())) {
            throw new HoldExpired(hold.token());
        }
        Booking b = new Booking(hold.customerId(), hold.showId(), hold.seatIds());
        b.setState(BookingState.HELD);
        Money total = b.quote();
        PaymentResult auth = payments.charge(pr, total);
        if (!auth.ok()) {
            b.setState(BookingState.CANCELLED);
            bookings.save(b);
            hold.lockKeys().forEach(k -> locks.release(k, hold.token()));
            throw new PaymentFailed(auth.code());
        }
        b.attachPayment(auth.paymentId());
        b.setState(BookingState.CONFIRMED);
        bookings.save(b);
        hold.lockKeys().forEach(k -> locks.release(k, hold.token()));
        bus.publish(new BookingConfirmed(b.getId(), b.getCustomerId()));
        return b;
    }
}

SETNX with a token value (not just 1) lets release verify the holder before deleting, which avoids the classic "TTL expired, another client acquired, now I delete their lock" race. On payment failure the seats are released immediately so the cinema map repopulates without waiting for the TTL.

Design patterns used#

The booking domain layers cleanly onto five behavioural and creational patterns. Each isolates a dimension that varies independently of the booking critical path.

  • State (Booking lifecycle): BookingState models the legal transitions HELD -> CONFIRMED -> CHECKED_IN -> CANCELLED/REFUNDED. The state object owns the guard logic; you cannot check in a CANCELLED booking or refund one still HELD. Centralising the state machine prevents the conditional sprawl that would otherwise leak into payment, notification, and cancellation services.
  • Strategy (PricingStrategy): WeekdayPricing, WeekendPricing, ShowtimeMultiplierPricing, and GroupDiscountPricing all implement PricingStrategy.price(show, seats). The cinema picks the strategy at show creation time; the booking flow never branches on the strategy in use. New strategies slot in without touching the booking service.
  • Observer (notifications): BookingConfirmed, ShowReminderDue, and BookingCancelled events fan out through an EventBus to subscribers like EmailNotifier, SmsNotifier, PushNotifier, and LoyaltyAccrualService. A reminder one hour before the show is one new subscriber, not an edit to the booking service.
  • Factory (SeatType subtypes): a SeatFactory.create(type, row, col) returns a fully configured Seat with the right base price and amenity flags (cup holder, recliner motor, premium legroom). Hides the conditional that would otherwise leak into pricing, seat-map rendering, and reporting whenever a new tier (IMAX recliner, lounge pod) is added.
  • Command (cancellation refund chain): a cancellation is a RefundCommand queue: release seats, refund the gateway payment, credit loyalty points, send a cancellation email, update analytics. Each step is a command with execute and undo, so a partial failure can roll back the steps already executed and surface an operational alert.

Common variants & follow-ups#

Interviewers usually push beyond the single-cinema baseline to test how the design generalises and which decisions you would defer. The variants below show up in real loops.

  • Multi-screen, multi-city chain: introduce a Brand aggregate above Cinema; cross-city search lands on a federated inventory index. Loyalty points and corporate offers roll up across cities. Discuss whether bookings are owned by the cinema or the brand.
  • Group booking with adjacent-seat constraint: a 6-seat booking must land on contiguous seats in the same row (or split across two adjacent rows). Model the seat-map as a 2D grid; run a sliding-window search for k consecutive AVAILABLE seats before offering options to the user.
  • Food and ticket combo: a Combo aggregate attaches concession items (popcorn, drinks) to the booking. Combos are priced independently and fulfilled on a different queue (concession counter pickup); failure of food fulfillment must not invalidate the ticket.
  • Refund-with-credit vs refund-to-card: cancellation policy offers store credit at 100 percent or card refund at 80 percent. Encoded as a RefundStrategy attached to the rate plan, not hard-coded; loyalty members may get richer terms.
  • Dynamic pricing (yield management): replace the static PricingStrategy with a YieldPricing that reads current occupancy, days-to-show, and competitor data. Cache quotes for 30 to 120 seconds so a search session sees stable prices while the cart is open.
  • Assigned vs unassigned seats: some cinemas (and most festival screenings) sell first-come-first-served general admission. Model Auditorium.seatingMode as ASSIGNED vs OPEN; in open mode the booking holds count, not specific seats, and the seat-map UI is skipped.
  • IMAX, 3D, and format selection: Show.format becomes part of the price key; glasses-deposit handling for 3D is a folio line on the booking; IMAX upgrades are a separate PricingStrategy overlay applied on top of the day-of-week base.

Edge cases#

These are the failure modes interviewers reliably probe; each one tests a different concurrency or domain invariant.

  • Race on the same seat by two users: two clients call selectSeats for the same seat at the same instant. The Redis SETNX (or equivalently a unique constraint on (show_id, seat_id, state=HELD) in Postgres) gives "first writer wins" semantics across replicas; the loser sees SeatUnavailable and the UI repaints the map. Pessimistic distributed locking is the correct call here because contention is high (popular shows) and the cost of a double-book is reputational, not just commercial.
  • Payment timeout while seats are locked: gateway latency spikes and the 8-minute TTL nearly expires during confirmBooking. The lock validation right after the gateway returns must check the token is still ours; if the TTL expired and another client grabbed it, the gateway charge is reversed (refund) and the booking lands in CANCELLED with a clear error to the customer. Keep the TTL well above p99 gateway latency or refresh the lock once on entry.
  • Show cancellation policy (auto-refund all): a print fails, projector dies, or the show is pulled. The operator triggers a ShowCancellation event; every CONFIRMED booking for that show flips to REFUNDED; refunds queue to the gateway; seats are released; customers receive an SMS plus email with rebooking credit. No human-in-the-loop per booking.
  • Cinema power failure mid-show: bookings already CHECKED_IN get a 100 percent rebooking credit (no refund-to-card by default; encoded as MidShowFailurePolicy); future shows for the rest of the day are auto-cancelled if the screen cannot resume within 30 minutes. The same ShowCancellation flow runs for the impacted shows.
  • Fraudulent booking detection: velocity rules flag a customer who books 20 seats across 5 shows in 60 seconds, or who pays with a card that fails a downstream chargeback dispute within the cancellation window. The FraudCheck runs asynchronously after confirmation; on positive signal the booking flips to FRAUD_HOLD, the ticket QR is invalidated, and the seat goes back to the inventory for the next sale.
  • Partial cancellation of a multi-seat booking: a 4-seat booking where 2 attendees can no longer come. Refund 2 of the 4 seats per the cancellation policy, release those 2 seats back to inventory, recompute taxes, and keep the booking CONFIRMED for the remaining 2 seats; the QR ticket is reissued with the updated seat list so theatre staff scanning at entry see the right count.

Glossary & fundamentals#

Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.

Tag Concept What it is Page
HLD Idempotency & retries safe re-execution, backoff + jitter idempotency-retries

Quick reference#

Functional#

  • Browse theatres/movies/shows.
  • View seat map; hold seats; pay; book.
  • Cancel; refund.

Patterns#

  • Saga (booking).
  • Strategy (pricing).
  • State (seat / booking).
  • Repository (DB access).

Trade-offs#

  • TTL holds prevent leaks; require sweep.
  • Strong consistency on seats is mandatory.

Refs#

FAQ#

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

Wrap the seat-hold step in a database transaction with SELECT FOR UPDATE on the seat row, or use a unique constraint on (show_id, seat_id) so only one insert can win.

How does the seat-hold-then-pay flow work?#

On select, mark seats as HELD with a short TTL (around 5 to 10 minutes); on payment success, transition to BOOKED; if the TTL expires first, release the hold automatically.

What states does a booking go through?#

PENDING (held), CONFIRMED (paid), CANCELLED (user cancelled), and REFUNDED (post-cancellation refund completed). Each transition is recorded with a timestamp.

How do you model seat categories and pricing?#

Seats belong to a SeatCategory (Standard, Premium, Recliner); Show overrides base prices per category. Booking total sums category price times selected seat count.

How are refunds handled?#

Cancellation triggers a Refund record processed asynchronously via the payment gateway; the seats flip back to AVAILABLE only after refund initiation to avoid duplicate sale.