Design a movie ticket booking system — full class-level s… — Cracked Java
// Low-Level Design (LLD / OOD) · Design a Movie Ticket Booking System (BookMyShow)
SeniorSystem DesignBig TechAmazonGoogle

Design a movie ticket booking system — full class-level solution.

1. Functional requirements

  • Search shows by city, cinema, and movie, filtered by date/time.
  • Display a seat map for a show with live availability.
  • Hold selected seats for a user for a fixed TTL (e.g., 15 minutes) while they pay.
  • Take payment (card / wallet / UPI) and confirm the booking, issuing tickets.
  • Auto-release held seats whose TTL expires without payment.
  • Cancel a confirmed booking and trigger a refund.

2. Non-functional requirements

  • No double-booking — a seat for a given show is sold at most once, even under concurrent requests. This is the dominant constraint.
  • No inventory leak — held-but-unpaid seats must return to the pool automatically.
  • Read-heavy — seat maps are viewed far more than booked; availability reads should be cheap.
  • Extensibility — new pricing rules and payment methods should not touch booking logic.

3. Core entities

EntityResponsibility
City / Cinema / CinemaHallLocation hierarchy; a hall has a fixed physical seat layout.
MovieTitle, duration, language, rating.
ShowA Movie in a CinemaHall at a start time; owns the bookable Seats.
SeatA seat for a specific show; holds SeatStatus + a version for CAS.
BookingA user's reservation of seats for a show; has a BookingState.
PaymentPayment attempt for a booking; created by a PaymentFactory.
UserThe customer making the booking.
PricingStrategyComputes seat price (early-bird, weekend, premium).
SeatLockManagerAtomically holds/releases seats and enforces the TTL.

4. Class diagram

Movie booking class model

5. Key interfaces and classes

enum SeatStatus { AVAILABLE, HELD, BOOKED }
enum SeatType   { REGULAR, PREMIUM, RECLINER }

interface PricingStrategy {                 // Strategy — pricing varies independently
    Money price(Show show, Seat seat);
}

final class WeekendPricing implements PricingStrategy {
    public Money price(Show show, Seat seat) {
        Money base = seat.type().basePrice();
        boolean weekend = isWeekend(show.startTime());
        return weekend ? base.multiply(1.25) : base;
    }
}
/** Optimistic hold: compare-and-set on each seat's version, all-or-nothing. */
final class SeatLockManager {
    // seatKey -> (userId, expiry); concurrent for read-heavy access
    private final ConcurrentHashMap<String, Hold> holds = new ConcurrentHashMap<>();
    private final Duration ttl = Duration.ofMinutes(15);

    boolean lock(Show show, List<Seat> seats, String userId) {
        List<String> acquired = new ArrayList<>();
        for (Seat seat : seats) {
            String key = show.id() + ":" + seat.id();
            Hold existing = holds.get(key);
            if (existing != null && !existing.expired()) {           // someone else holds it
                rollback(acquired);
                return false;
            }
            Hold mine = new Hold(userId, Instant.now().plus(ttl));
            if (holds.putIfAbsent(key, mine) != null                 // CAS-style claim
                && !replaceIfExpired(key, mine)) {
                rollback(acquired);
                return false;
            }
            seat.markHeld();
            acquired.add(key);
        }
        return true;                                                 // all seats held atomically
    }

    void commit(Show show, List<Seat> seats) {                       // payment succeeded
        for (Seat s : seats) { s.markBooked(); holds.remove(show.id() + ":" + s.id()); }
    }
    void unlock(Show show, List<Seat> seats, String userId) { /* release + reset to AVAILABLE */ }
    private void rollback(List<String> keys) { keys.forEach(holds::remove); }
}
interface BookingState {                    // State — booking lifecycle
    void confirm(Booking b);
    void cancel(Booking b);
}

final class PendingState implements BookingState {
    public void confirm(Booking b) {
        if (b.payment().isSettled()) {
            b.lockManager().commit(b.show(), b.seats());
            b.setState(new ConfirmedState());
        } else throw new IllegalStateException("payment not settled");
    }
    public void cancel(Booking b) {
        b.lockManager().unlock(b.show(), b.seats(), b.user().id());  // release holds
        b.setState(new CancelledState());
    }
}

6. Design patterns used

  • StateBooking moves through Pending → Confirmed → Cancelled; each state defines what confirm/cancel mean and forbids the rest. Confirming releases nothing; cancelling releases holds or refunds.
  • StrategyPricingStrategy (early-bird, weekend, premium) lets pricing vary per show/seat without editing booking code.
  • FactoryPaymentFactory creates the right Payment (card/wallet/UPI) so adding a method touches one place.
  • Observer — clients subscribe to a show's seat-availability changes to live-update the seat map when seats are held/booked/released.

7. Trade-offs and alternatives

The seat-contention problem has three standard answers:

  • Pessimistic lockingSELECT ... FOR UPDATE on the seat rows (or a per-show lock) for the duration of the booking. Simplest to reason about and guarantees no double-book, but holds DB locks across a slow user payment step, hurting throughput and risking lock timeouts. Acceptable when contention is high and bookings are short.
  • Optimistic locking — each Seat carries a version column; booking reads the version, then UPDATE ... WHERE id = ? AND version = ?. If zero rows update, someone else won — retry or fail. No locks held during think-time; best when contention is low (most shows). The skeleton above is the in-memory analogue (CAS on a hold map).
  • Seat-hold with TTL — decouple hold from pay. The user atomically reserves seats (status HELD, 15-minute expiry) before paying; on payment success the hold commits to BOOKED, on expiry a sweeper releases it. This is what real systems use because it solves both double-booking and the "user abandons checkout" inventory leak. Holds live in Redis with a native TTL key in production.

Pricing precision uses a Money value object (minor-unit long), never double. The seat map is read-heavy, so availability is cached and invalidated on hold/commit events (the Observer feed).

8. Common follow-up questions

  • Seat-hold TTL (15 min) — Redis key with EXPIRE; a background sweeper (or Redis keyspace notification) flips expired holds back to AVAILABLE and fires the Observer event.
  • Waitlist — when a show is full, queue users; on cancellation, offer released seats to the head of the queue with a short claim window.
  • Group booking — atomic all-or-nothing hold across N adjacent seats; the lock method already rolls back partial acquisition.
  • RefundsCancelledState triggers a refund Payment via the Factory; refund amount depends on a cancellation-policy Strategy (time-before-show tiers).
  • Dynamic pricing — a PricingStrategy that factors current occupancy/demand, recomputed per request.
  • Multi-city search — index shows by (city, movie, date); the location hierarchy makes this a straightforward filter.

9. What interviewers are really probing

Mark your status