Episode 9 — System Design / 9.6 — LLD Problem Solving

9.6.c — BookMyShow (Movie Booking System)

Problem: Design a movie ticket booking platform where users browse movies, select a show, pick seats, and make a booking. The system must handle concurrent seat selection without double-booking.


Table of Contents

  1. Requirements
  2. Entities and Enums
  3. Class Diagram
  4. Sequence Flows
  5. Complete Implementation
  6. Usage Walkthrough
  7. Edge Cases
  8. Key Takeaways

Requirements

Functional Requirements

  1. The system manages multiple theatres in multiple cities.
  2. Each theatre has multiple screens.
  3. Each screen has a fixed layout of seats (rows and columns).
  4. Movies are scheduled as shows on specific screens at specific times.
  5. Users can browse shows for a movie, select seats, and book them.
  6. Selected seats are temporarily locked (5 min) to prevent double-booking.
  7. If payment succeeds, the booking is confirmed; otherwise, the lock is released.
  8. Users can cancel a confirmed booking.

Non-Functional Requirements

  • Concurrent seat selection must be safe (no two users can book the same seat).
  • Extensible seat pricing (regular, premium, VIP).

Out of Scope

  • Payment gateway integration (simulated).
  • Recommendations, reviews, ratings.
  • Food ordering.

Entities and Enums

┌────────────────────┐    ┌────────────────────┐    ┌────────────────────┐
│    SeatType         │    │   SeatStatus        │    │  BookingStatus      │
│────────────────────│    │────────────────────│    │────────────────────│
│  REGULAR            │    │  AVAILABLE          │    │  PENDING            │
│  PREMIUM            │    │  LOCKED             │    │  CONFIRMED          │
│  VIP                │    │  BOOKED             │    │  CANCELLED          │
└────────────────────┘    └────────────────────┘    └────────────────────┘

┌────────────────────┐
│   PaymentStatus     │
│────────────────────│
│  PENDING            │
│  SUCCESS            │
│  FAILED             │
└────────────────────┘

Class Diagram

┌──────────────────────────────────┐
│            Movie                  │
│──────────────────────────────────│
│ - id: string                      │
│ - title: string                   │
│ - durationMin: number             │
│ - genre: string                   │
│ - language: string                │
└──────────────┬───────────────────┘
               │ shown in
               ▼
┌──────────────────────────────────┐       ┌──────────────────────────────┐
│            Show                   │       │          Theatre              │
│──────────────────────────────────│       │──────────────────────────────│
│ - id: string                      │◄──────│ - id: string                 │
│ - movie: Movie                    │       │ - name: string               │
│ - screen: Screen                  │       │ - city: string               │
│ - startTime: Date                 │       │ - screens: Screen[]          │
│ - endTime: Date                   │       │──────────────────────────────│
│ - seatPricing: Map<SeatType,num> │       │ + addScreen(screen): void    │
│ - seatStatus: Map<seatId, status>│       └──────────────┬───────────────┘
│──────────────────────────────────│                      │ has-many
│ + getAvailableSeats(): Seat[]     │                      ▼
│ + lockSeats(seatIds, userId): bool│       ┌──────────────────────────────┐
│ + bookSeats(seatIds): void        │       │          Screen               │
│ + releaseSeats(seatIds): void     │       │──────────────────────────────│
└──────────────────────────────────┘       │ - id: string                  │
                                            │ - name: string                │
                                            │ - seats: Seat[]               │
                                            │──────────────────────────────│
                                            │ + getSeatMap(): Seat[][]      │
                                            └──────────────┬───────────────┘
                                                           │ has-many
                                                           ▼
┌──────────────────────────────────┐       ┌──────────────────────────────┐
│           Booking                 │       │           Seat                │
│──────────────────────────────────│       │──────────────────────────────│
│ - id: string                      │       │ - id: string                  │
│ - user: User                      │       │ - row: string                 │
│ - show: Show                      │       │ - number: number              │
│ - seats: Seat[]                   │       │ - type: SeatType              │
│ - totalPrice: number              │       └──────────────────────────────┘
│ - status: BookingStatus           │
│ - createdAt: Date                 │
│──────────────────────────────────│       ┌──────────────────────────────┐
│ + confirm(): void                 │       │           User                │
│ + cancel(): void                  │       │──────────────────────────────│
└──────────────────────────────────┘       │ - id: string                  │
                                            │ - name: string                │
┌──────────────────────────────────┐       │ - email: string               │
│       BookingService              │       └──────────────────────────────┘
│──────────────────────────────────│
│ - bookings: Map<id, Booking>      │
│ - lockTimeoutMs: number           │
│──────────────────────────────────│
│ + startBooking(user, show, seats) │
│ + confirmBooking(bookingId): bool │
│ + cancelBooking(bookingId): void  │
│ + cleanExpiredLocks(): void       │
└──────────────────────────────────┘

Sequence Flows

Seat Selection and Booking Flow

  User        BookingService        Show            Seat          Payment
    │               │                 │               │               │
    │── browse ────►│                 │               │               │
    │◄── shows[] ───│                 │               │               │
    │               │                 │               │               │
    │── getSeats() ►│── available? ──►│               │               │
    │◄── seatMap ───│◄── seats[] ────│               │               │
    │               │                 │               │               │
    │── startBooking(show, [A1,A2])──►│               │               │
    │               │── lockSeats() ─►│               │               │
    │               │                 │── lock A1,A2 ─►│              │
    │               │◄── locked ──────│◄── ok ────────│               │
    │◄── booking    │                 │               │               │
    │   (PENDING)   │                 │               │               │
    │               │                 │               │               │
    │── confirmBooking(id) ──────────►│               │               │
    │               │── processPayment() ────────────────────────────►│
    │               │◄── SUCCESS ────────────────────────────────────│
    │               │── bookSeats() ─►│               │               │
    │               │                 │── book A1,A2 ─►│              │
    │◄── CONFIRMED  │◄── done ───────│◄── ok ────────│               │

Lock Expiry Flow

  Timer          BookingService        Show
    │                  │                 │
    │── tick (5 min) ─►│                 │
    │                  │── cleanExpiredLocks()
    │                  │── find expired ─►│
    │                  │                 │── release seats
    │                  │── cancel booking │
    │                  │◄── done ────────│

Complete Implementation

Enums

// ─── Enums ──────────────────────────────────────────────

const SeatType = Object.freeze({
  REGULAR: 'REGULAR',
  PREMIUM: 'PREMIUM',
  VIP: 'VIP',
});

const SeatStatus = Object.freeze({
  AVAILABLE: 'AVAILABLE',
  LOCKED: 'LOCKED',
  BOOKED: 'BOOKED',
});

const BookingStatus = Object.freeze({
  PENDING: 'PENDING',
  CONFIRMED: 'CONFIRMED',
  CANCELLED: 'CANCELLED',
});

const PaymentStatus = Object.freeze({
  PENDING: 'PENDING',
  SUCCESS: 'SUCCESS',
  FAILED: 'FAILED',
});

Core Entities

// ─── User ───────────────────────────────────────────────

class User {
  static _counter = 0;

  constructor(name, email) {
    this.id = `U-${++User._counter}`;
    this.name = name;
    this.email = email;
  }
}

// ─── Movie ──────────────────────────────────────────────

class Movie {
  static _counter = 0;

  constructor(title, durationMin, genre, language) {
    this.id = `MOV-${++Movie._counter}`;
    this.title = title;
    this.durationMin = durationMin;
    this.genre = genre;
    this.language = language;
  }
}

// ─── Seat ───────────────────────────────────────────────

class Seat {
  constructor(row, number, type = SeatType.REGULAR) {
    this.id = `${row}${number}`;
    this.row = row;
    this.number = number;
    this.type = type;
  }
}

Screen

// ─── Screen ─────────────────────────────────────────────

class Screen {
  static _counter = 0;

  constructor(name, seatLayout) {
    this.id = `SCR-${++Screen._counter}`;
    this.name = name;
    this.seats = [];

    // seatLayout example:
    // [
    //   { row: 'A', count: 10, type: SeatType.VIP },
    //   { row: 'B', count: 10, type: SeatType.PREMIUM },
    //   { row: 'C', count: 12, type: SeatType.REGULAR },
    //   { row: 'D', count: 12, type: SeatType.REGULAR },
    // ]
    for (const config of seatLayout) {
      for (let i = 1; i <= config.count; i++) {
        this.seats.push(new Seat(config.row, i, config.type));
      }
    }
  }

  getSeatById(seatId) {
    return this.seats.find(s => s.id === seatId) || null;
  }

  getSeatMap() {
    const map = {};
    for (const seat of this.seats) {
      if (!map[seat.row]) map[seat.row] = [];
      map[seat.row].push(seat);
    }
    return map;
  }
}

Theatre

// ─── Theatre ────────────────────────────────────────────

class Theatre {
  static _counter = 0;

  constructor(name, city) {
    this.id = `TH-${++Theatre._counter}`;
    this.name = name;
    this.city = city;
    this.screens = [];
  }

  addScreen(screen) {
    this.screens.push(screen);
  }
}

Show

// ─── Show ───────────────────────────────────────────────
// A show is a specific screening of a movie on a screen at a time.
// It tracks seat status independently (same screen can have different
// shows with different seat availability).

class Show {
  static _counter = 0;

  constructor(movie, screen, startTime, seatPricing) {
    this.id = `SHOW-${++Show._counter}`;
    this.movie = movie;
    this.screen = screen;
    this.startTime = startTime;
    this.endTime = new Date(startTime.getTime() + movie.durationMin * 60000);

    // seatPricing: { REGULAR: 200, PREMIUM: 350, VIP: 500 }
    this.seatPricing = seatPricing;

    // Track status per seat for THIS show
    // seatId -> { status, lockedBy, lockedAt }
    this.seatStatus = new Map();
    for (const seat of screen.seats) {
      this.seatStatus.set(seat.id, {
        status: SeatStatus.AVAILABLE,
        lockedBy: null,
        lockedAt: null,
      });
    }
  }

  getAvailableSeats() {
    const available = [];
    for (const seat of this.screen.seats) {
      const info = this.seatStatus.get(seat.id);
      if (info.status === SeatStatus.AVAILABLE) {
        available.push({
          ...seat,
          price: this.seatPricing[seat.type] || 0,
        });
      }
    }
    return available;
  }

  getSeatPrice(seatId) {
    const seat = this.screen.getSeatById(seatId);
    return seat ? (this.seatPricing[seat.type] || 0) : 0;
  }

  // Lock seats for a user (temporary hold)
  lockSeats(seatIds, userId) {
    // First verify all seats are available
    for (const seatId of seatIds) {
      const info = this.seatStatus.get(seatId);
      if (!info || info.status !== SeatStatus.AVAILABLE) {
        return false; // At least one seat not available
      }
    }

    // All available — lock them atomically
    const now = new Date();
    for (const seatId of seatIds) {
      this.seatStatus.set(seatId, {
        status: SeatStatus.LOCKED,
        lockedBy: userId,
        lockedAt: now,
      });
    }
    return true;
  }

  // Confirm booking — move from LOCKED to BOOKED
  bookSeats(seatIds) {
    for (const seatId of seatIds) {
      const info = this.seatStatus.get(seatId);
      if (info && info.status === SeatStatus.LOCKED) {
        this.seatStatus.set(seatId, {
          status: SeatStatus.BOOKED,
          lockedBy: null,
          lockedAt: null,
        });
      }
    }
  }

  // Release seats (cancel or lock expiry)
  releaseSeats(seatIds) {
    for (const seatId of seatIds) {
      this.seatStatus.set(seatId, {
        status: SeatStatus.AVAILABLE,
        lockedBy: null,
        lockedAt: null,
      });
    }
  }

  // Find all expired locks
  getExpiredLocks(timeoutMs) {
    const now = Date.now();
    const expired = [];
    for (const [seatId, info] of this.seatStatus) {
      if (
        info.status === SeatStatus.LOCKED &&
        info.lockedAt &&
        now - info.lockedAt.getTime() > timeoutMs
      ) {
        expired.push({ seatId, userId: info.lockedBy });
      }
    }
    return expired;
  }
}

Booking

// ─── Booking ────────────────────────────────────────────

class Booking {
  static _counter = 0;

  constructor(user, show, seatIds) {
    this.id = `BK-${++Booking._counter}`;
    this.user = user;
    this.show = show;
    this.seatIds = seatIds;
    this.totalPrice = seatIds.reduce((sum, id) => sum + show.getSeatPrice(id), 0);
    this.status = BookingStatus.PENDING;
    this.createdAt = new Date();
  }

  confirm() {
    this.status = BookingStatus.CONFIRMED;
  }

  cancel() {
    this.status = BookingStatus.CANCELLED;
  }
}

BookingService (Controller)

// ─── BookingService ─────────────────────────────────────

class BookingService {
  constructor(lockTimeoutMs = 5 * 60 * 1000) {
    this.bookings = new Map();       // bookingId -> Booking
    this.lockTimeoutMs = lockTimeoutMs;
  }

  // Step 1: User selects seats. Lock them and create a PENDING booking.
  startBooking(user, show, seatIds) {
    // Attempt to lock seats
    const locked = show.lockSeats(seatIds, user.id);
    if (!locked) {
      throw new Error('One or more seats are not available. Please select different seats.');
    }

    const booking = new Booking(user, show, seatIds);
    this.bookings.set(booking.id, booking);

    console.log(
      `[BOOKING] ${booking.id} created for ${user.name}. ` +
      `Seats: ${seatIds.join(', ')}. Total: $${booking.totalPrice}. ` +
      `Status: PENDING (locked for ${this.lockTimeoutMs / 60000} min)`
    );

    return booking;
  }

  // Step 2: User pays. Confirm the booking.
  confirmBooking(bookingId) {
    const booking = this.bookings.get(bookingId);
    if (!booking) throw new Error(`Booking ${bookingId} not found.`);
    if (booking.status !== BookingStatus.PENDING) {
      throw new Error(`Booking ${bookingId} is ${booking.status}, cannot confirm.`);
    }

    // Simulate payment
    const paymentSuccess = this._processPayment(booking);

    if (paymentSuccess) {
      booking.show.bookSeats(booking.seatIds);
      booking.confirm();
      console.log(`[CONFIRMED] ${booking.id} for ${booking.user.name}.`);
      return true;
    } else {
      // Payment failed — release seats
      booking.show.releaseSeats(booking.seatIds);
      booking.cancel();
      console.log(`[PAYMENT FAILED] ${booking.id}. Seats released.`);
      return false;
    }
  }

  // Cancel a confirmed or pending booking
  cancelBooking(bookingId) {
    const booking = this.bookings.get(bookingId);
    if (!booking) throw new Error(`Booking ${bookingId} not found.`);

    if (booking.status === BookingStatus.CANCELLED) {
      throw new Error('Booking is already cancelled.');
    }

    booking.show.releaseSeats(booking.seatIds);
    booking.cancel();
    console.log(`[CANCELLED] ${booking.id}. Seats released. Refund: $${booking.totalPrice}`);
  }

  // Clean up expired locks (call periodically)
  cleanExpiredLocks(shows) {
    for (const show of shows) {
      const expired = show.getExpiredLocks(this.lockTimeoutMs);
      if (expired.length > 0) {
        const seatIds = expired.map(e => e.seatId);
        show.releaseSeats(seatIds);
        console.log(`[CLEANUP] Released expired locks: ${seatIds.join(', ')} for show ${show.id}`);
      }
    }

    // Cancel pending bookings whose seats were released
    for (const [id, booking] of this.bookings) {
      if (booking.status === BookingStatus.PENDING) {
        const elapsed = Date.now() - booking.createdAt.getTime();
        if (elapsed > this.lockTimeoutMs) {
          booking.cancel();
          console.log(`[CLEANUP] Cancelled expired booking ${id}`);
        }
      }
    }
  }

  // Simulated payment
  _processPayment(booking) {
    // In real system: call payment gateway
    console.log(`[PAYMENT] Processing $${booking.totalPrice} for ${booking.user.name}...`);
    return true; // Always succeeds in simulation
  }
}

Usage Walkthrough

// ─── Demo ───────────────────────────────────────────────

// Setup: Theatre with a screen
const screen1 = new Screen('Audi 1', [
  { row: 'A', count: 8, type: SeatType.VIP },
  { row: 'B', count: 10, type: SeatType.PREMIUM },
  { row: 'C', count: 12, type: SeatType.REGULAR },
  { row: 'D', count: 12, type: SeatType.REGULAR },
]);

const theatre = new Theatre('PVR Cinemas', 'Mumbai');
theatre.addScreen(screen1);

// Schedule a movie
const movie = new Movie('Inception', 148, 'Sci-Fi', 'English');
const show = new Show(movie, screen1, new Date('2026-04-12T18:00:00'), {
  [SeatType.REGULAR]: 200,
  [SeatType.PREMIUM]: 350,
  [SeatType.VIP]: 500,
});

// Booking service
const service = new BookingService(5 * 60 * 1000); // 5 min lock

// User 1 browses and books
const user1 = new User('Alice', 'alice@example.com');
console.log('Available seats:', show.getAvailableSeats().length); // 42

const booking1 = service.startBooking(user1, show, ['A1', 'A2']);
// [BOOKING] BK-1 created for Alice. Seats: A1, A2. Total: $1000. Status: PENDING

service.confirmBooking(booking1.id);
// [PAYMENT] Processing $1000 for Alice...
// [CONFIRMED] BK-1 for Alice.

// User 2 tries the same seats
const user2 = new User('Bob', 'bob@example.com');
try {
  service.startBooking(user2, show, ['A1', 'A3']);
  // A1 is BOOKED, so this fails
} catch (err) {
  console.log(err.message);
  // "One or more seats are not available. Please select different seats."
}

// User 2 picks different seats
const booking2 = service.startBooking(user2, show, ['A3', 'A4']);
service.confirmBooking(booking2.id);

// User 1 cancels
service.cancelBooking(booking1.id);
// [CANCELLED] BK-1. Seats released. Refund: $1000

// Now A1, A2 are available again
console.log('A1 status:', show.seatStatus.get('A1').status); // AVAILABLE

Edge Cases

Edge CaseHow It Is Handled
Two users select the same seat simultaneouslylockSeats() checks all seats first; only the first caller succeeds
Lock expires before paymentcleanExpiredLocks() releases seats and cancels booking
Payment fails after lockconfirmBooking() releases seats and marks booking CANCELLED
User cancels a confirmed bookingcancelBooking() releases seats and marks CANCELLED
User tries to book 0 seatsstartBooking with empty array creates a $0 booking (add validation)
Show time has passedAdd time validation in startBooking (not shown for brevity)
Same user books same seat twiceSeat is already BOOKED/LOCKED, lockSeats() returns false

Concurrency Note

In a real system (multi-threaded / distributed), lockSeats() would use:

  • Database-level row locking (SELECT ... FOR UPDATE)
  • Optimistic concurrency (version numbers on seat rows)
  • Redis distributed lock for the seat selection window

In JavaScript (single-threaded), the sequential check-then-lock in lockSeats() is sufficient.


Key Takeaways

  1. Seat locking is the critical design decision — it prevents double-booking without requiring pessimistic locks on the entire show.
  2. The Show entity owns seat status (not the Screen), because the same screen can have different shows with independent availability.
  3. BookingService acts as the orchestrator — it coordinates between Show, Booking, and Payment.
  4. Temporal lock expiry is essential: without it, abandoned carts would permanently hold seats.
  5. In interviews, always discuss the concurrency strategy even if your implementation is single-threaded — it shows you understand real-world constraints.

Next -> 9.6.d — Splitwise