Case Studies

Movie Ticket Booking (BookMyShow)

Design a movie booking system with seat selection, holds, payments, concurrent users.

5 min readDesigns · LLD & Patterns

Interview practice: try designing this yourself before reading. Go to the exercise ↓

Problem Statement

Design a movie ticket booking system (BookMyShow / Fandango) that:

  • Lists cities → cinemas → screens → shows for a movie
  • Lets users select seats from a screen layout
  • Holds seats for ~5 minutes during checkout
  • Processes payment and confirms booking
  • Handles concurrent users selecting the same seat

Requirements

Functional

  • Browse movies by city / language / genre
  • Search shows for a movie
  • Pick seats (check availability, type)
  • Hold and release seats
  • Pay & confirm
  • Cancel booking (refund per policy)

Non-Functional

  • Two users can't book the same seat
  • Hold expires automatically (no manual cleanup)
  • High read traffic (everyone browsing); spiky write traffic on popular shows

Class Diagram


Show + Seat Layout

public enum SeatType { REGULAR, PREMIUM, RECLINER }

public class Seat {
    public final String id;
    public final int row;
    public final int column;
    public final SeatType type;
}

public enum SeatStatus { AVAILABLE, HELD, BOOKED }

public class Show {
    private final String id;
    private final Movie movie;
    private final Screen screen;
    private final Instant startTime;
    private final Map<SeatType, Money> pricing;

    // Per-show seat state — important to be per-show not per-seat
    private final ConcurrentMap<String, SeatState> seatStates = new ConcurrentHashMap<>();

    private static class SeatState {
        SeatStatus status = SeatStatus.AVAILABLE;
        String holdId;       // who holds it
        Instant heldUntil;
    }

    public Show(Movie m, Screen s, Instant start, Map<SeatType, Money> pricing) {
        this.movie = m; this.screen = s; this.startTime = start;
        this.pricing = pricing;
        this.id = UUID.randomUUID().toString();
        for (Seat seat : s.getSeats()) {
            seatStates.put(seat.id, new SeatState());
        }
    }

    public Money priceFor(Seat seat) { return pricing.get(seat.type); }

    SeatState getState(String seatId) { return seatStates.get(seatId); }
}

Seat status lives on the Show, not the Seat. The same physical seat is BOOKED in show A and AVAILABLE in show B simultaneously.


Seat Holds (the Critical Section)

The classic concurrency bug: two users click the same seat at the same time. Solve with atomic state transition:

public class SeatHold {
    public final String id;
    public final Show show;
    public final List<String> seatIds;
    public final User user;
    public final Instant expiresAt;

    public SeatHold(Show show, List<String> seatIds, User user, Duration ttl) {
        this.id = UUID.randomUUID().toString();
        this.show = show;
        this.seatIds = List.copyOf(seatIds);
        this.user = user;
        this.expiresAt = Instant.now().plus(ttl);
    }

    public boolean isExpired() { return Instant.now().isAfter(expiresAt); }
}
public class BookingService {
    private static final Duration HOLD_TTL = Duration.ofMinutes(5);
    private final Map<String, SeatHold> holds = new ConcurrentHashMap<>();
    private final ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor();

    public BookingService() {
        cleaner.scheduleAtFixedRate(this::reapExpiredHolds, 30, 30, TimeUnit.SECONDS);
    }

    public SeatHold holdSeats(Show show, List<String> seatIds, User user) {
        // Lock per-show to serialize hold operations on the same show
        synchronized (show) {
            // Check all are AVAILABLE
            for (String sid : seatIds) {
                Show.SeatState s = show.getState(sid);
                if (s == null) throw new UnknownSeatException(sid);
                if (s.status != SeatStatus.AVAILABLE) {
                    if (s.status == SeatStatus.HELD && Instant.now().isAfter(s.heldUntil)) {
                        // Lazy expire
                        s.status = SeatStatus.AVAILABLE;
                    } else {
                        throw new SeatUnavailableException(sid);
                    }
                }
            }
            // Atomically flip to HELD
            SeatHold hold = new SeatHold(show, seatIds, user, HOLD_TTL);
            for (String sid : seatIds) {
                Show.SeatState s = show.getState(sid);
                s.status = SeatStatus.HELD;
                s.holdId = hold.id;
                s.heldUntil = hold.expiresAt;
            }
            holds.put(hold.id, hold);
            return hold;
        }
    }

    public Booking confirm(SeatHold hold, Payment payment) {
        synchronized (hold.show) {
            if (hold.isExpired()) throw new HoldExpiredException();
            // Re-verify seats still belong to this hold
            for (String sid : hold.seatIds) {
                Show.SeatState s = hold.show.getState(sid);
                if (s.status != SeatStatus.HELD || !hold.id.equals(s.holdId))
                    throw new IllegalStateException("hold lost");
            }
            payment.process();   // throws on failure

            for (String sid : hold.seatIds) {
                Show.SeatState s = hold.show.getState(sid);
                s.status = SeatStatus.BOOKED;
                s.holdId = null;
                s.heldUntil = null;
            }
            holds.remove(hold.id);

            Money total = hold.seatIds.stream()
                .map(sid -> hold.show.priceFor(seatFor(sid)))
                .reduce(Money.zero(), Money::plus);

            return new Booking(hold.show, hold.seatIds, hold.user, payment, total);
        }
    }

    public void release(SeatHold hold) {
        synchronized (hold.show) {
            for (String sid : hold.seatIds) {
                Show.SeatState s = hold.show.getState(sid);
                if (hold.id.equals(s.holdId)) {
                    s.status = SeatStatus.AVAILABLE;
                    s.holdId = null;
                    s.heldUntil = null;
                }
            }
            holds.remove(hold.id);
        }
    }

    private void reapExpiredHolds() {
        for (SeatHold h : holds.values()) {
            if (h.isExpired()) release(h);
        }
    }
}

Seat Selection Lifecycle


Sequence: Two Concurrent Bookings


Concurrency Strategy

The service uses per-show synchronization for holds. This means:

  • Different shows don't block each other (high parallelism overall).
  • Heavily-booked shows do serialize, but each hold operation is millisecond-level.

Alternative: use a distributed lock (Redis SETNX) for multi-server deployments.

For DB-backed implementations: row-level locking (SELECT ... FOR UPDATE) on the seat row.


Booking & Cancellation

public class Booking {
    public final String id;
    public final Show show;
    public final List<String> seatIds;
    public final User user;
    public final Payment payment;
    public final Money total;
    private BookingStatus status = BookingStatus.CONFIRMED;
    public final Instant createdAt = Instant.now();
}

public enum BookingStatus { CONFIRMED, CANCELLED }

Cancellation:

  • Compute refund per policy (e.g., full refund if > 24h before show, partial if closer)
  • Process refund through payment gateway
  • Mark booking CANCELLED
  • Set seats back to AVAILABLE

Edge Cases

CaseHandling
Hold expires mid-paymentconfirm() throws; user retries from seat selection
Network split during confirmIdempotency key on payment to avoid double-charge
Show starts during browseReject hold for past shows
Group booking — all-or-nothingHold all seats atomically; if any fails, release all
Refund after partial show startPolicy decision; usually no refund
Payment succeeds but DB write failsReconciliation job; refund or replay

Design Patterns Used

PatternWhere
StateSeatStatus lifecycle (Available / Held / Booked)
StrategyPricing per seat type; cancellation refund policy
FacadeBookingService
ObserverShow countdown to hold expiry on user UI
SingletonOne BookingService per app
RepositoryPersistence for shows, holds, bookings

Interview Tips

  • The seat hold is the central design challenge — interviewers ask about concurrency here.
  • Per-show locking is fine; mention scaling via row-level DB locks or distributed locks (Redis).
  • Lazy expiration on read + scheduled sweep covers both correctness and cleanup.
  • Mention idempotency for payments — the payment must not double-charge if the user retries.
  • Distinguish from generic ticket booking: movie has fixed seat layout; some events (concerts) have GA sections that just need a counter.

Practice

Try it yourself

Design the classes for a movie booking system with seat selection and payment.

Your design should cover

  • Cinemas, screens, shows and a seat layout per screen
  • Select seats and hold them for a few minutes during payment
  • Two users can never book the same seat
  • Holds expire automatically if payment does not complete
  • Cancel a booking and release the seats
0 / 8,000 characters · saved in this browser

Finished reading?

Mark it done to track your progress in Designs.