LLD Hub
lldconcurrencystate-machinestrategy

Movie Ticket Booking Low Level Design (BookMyShow) — LLD Guide

Design a movie ticket booking system with concurrent seat booking, seat locking, pricing strategy, and payment flow — asked at Swiggy, Razorpay, BookMyShow interviews.

3 April 2025·9 min read

Practice this problem

Movie Ticket Booking (BookMyShow) — get AI-scored feedback on your solution

Solve it →

Movie Ticket Booking (BookMyShow) is one of the most concurrency-heavy Low Level Design problems in software engineering interviews. It requires seat locking, concurrent reservation safety, and a multi-stage booking flow. Frequently asked at Swiggy, Razorpay, BookMyShow, and Flipkart, this guide covers the complete LLD solution with Java code, class diagram, and FAQ.

Why Interviewers Ask Movie Ticket Booking LLD

This problem is a concurrency stress test wrapped in a familiar domain. Interviewers use it to see:

  • Can you model the seat locking flow — temporary hold, confirm, expire?
  • Do you prevent double-booking without a global lock that kills throughput?
  • Can you use Strategy pattern for dynamic and category-based pricing?
  • Do you separate Show, Screen, and Movie as distinct entities?
  • Can you design a booking state machine (PENDING → CONFIRMED → CANCELLED)?

Functional Requirements

  • Users can browse movies, cinemas, and shows for a given date and city
  • User selects a show and views the seat map with availability
  • User selects seats — system temporarily locks them for 10 minutes
  • User completes payment — booking is confirmed and seats are permanently reserved
  • If payment fails or times out, seat lock is released automatically
  • User can cancel a confirmed booking (refund policy applies)
  • Support multiple seat categories: Regular, Premium, Recliner

Non-Functional Requirements

  • No two users must be able to book the same seat for the same show
  • Seat lock must auto-expire after 10 minutes without a global scan loop
  • Seat availability queries must be fast — O(1) per show
  • System must handle thousands of concurrent seat selection requests

Core Entities — Movie Ticket Booking LLD Class Design

  • Movie — id, title, genre, duration, language
  • Cinema — id, name, city, list of screens
  • Screen — id, screenNumber, list of seats, seating layout
  • Show — id, movie, screen, startTime, pricing per category
  • Seat — id, row, column, category (REGULAR/PREMIUM/RECLINER), status
  • SeatLock — seatId, showId, userId, lockedAt, expiresAt
  • Booking — id, show, user, seats, totalAmount, status, paymentId
  • PricingStrategy — interface; CategoryPricing, SurgePricing implement it
  • Payment — bookingId, amount, method, status, timestamp

Text-Based Class Diagram

Movie
+-- id, title, genre, durationMinutes, language

Cinema
+-- id, name, city
+-- screens: List<Screen>

Screen
+-- id, screenNumber
+-- seats: List<Seat>
+-- totalCapacity: int

Show
+-- id, movie: Movie, screen: Screen
+-- startTime: LocalDateTime
+-- categoryPrices: Map<SeatCategory, Double>
+-- getAvailableSeats(): List<Seat>

Seat
+-- id, row: char, column: int
+-- category: SeatCategory (REGULAR/PREMIUM/RECLINER)
+-- status: SeatStatus (AVAILABLE/LOCKED/BOOKED)

SeatLock
+-- seatId, showId, userId
+-- lockedAt, expiresAt: LocalDateTime

Booking
+-- id, show: Show, user: User
+-- seats: List<Seat>
+-- totalAmount: double
+-- status: BookingStatus (PENDING/CONFIRMED/CANCELLED)
+-- paymentId: String

PricingStrategy (interface)
+-- calculatePrice(show, seats): double

CategoryPricing implements PricingStrategy
SurgePricing    implements PricingStrategy

Seat Locking with Concurrent Safety — Java

The seat locking service is the most critical component. It must guarantee atomicity — if two users click the same seat simultaneously, exactly one must succeed. Using a per-show lock (not a global lock) limits contention to users within the same show.

public class SeatLockService {
    private static final int LOCK_DURATION_MINUTES = 10;

    // One ReentrantLock per show — limits contention to the same show only
    private final ConcurrentHashMap<String, ReentrantLock> showLocks = new ConcurrentHashMap<>();
    private final Map<String, SeatLock> activeLocks = new HashMap<>(); // showId+seatId -> SeatLock

    private ReentrantLock getShowLock(String showId) {
        return showLocks.computeIfAbsent(showId, k -> new ReentrantLock());
    }

    public boolean lockSeats(String showId, List<String> seatIds, String userId) {
        ReentrantLock lock = getShowLock(showId);
        lock.lock();
        try {
            // Check all seats are available (not locked or booked)
            for (String seatId : seatIds) {
                String key = showId + ":" + seatId;
                SeatLock existing = activeLocks.get(key);
                if (existing != null && !existing.isExpired()) return false;
            }
            // Lock all seats atomically
            LocalDateTime now = LocalDateTime.now();
            LocalDateTime expiry = now.plusMinutes(LOCK_DURATION_MINUTES);
            for (String seatId : seatIds) {
                String key = showId + ":" + seatId;
                activeLocks.put(key, new SeatLock(seatId, showId, userId, now, expiry));
            }
            return true;
        } finally {
            lock.unlock();
        }
    }

    public void releaseLocks(String showId, List<String> seatIds) {
        ReentrantLock lock = getShowLock(showId);
        lock.lock();
        try {
            seatIds.forEach(id -> activeLocks.remove(showId + ":" + id));
        } finally {
            lock.unlock();
        }
    }

    public boolean isLockedByUser(String showId, String seatId, String userId) {
        SeatLock sl = activeLocks.get(showId + ":" + seatId);
        return sl != null && sl.getUserId().equals(userId) && !sl.isExpired();
    }
}

Pricing Strategy — Category and Surge Pricing

public interface PricingStrategy {
    double calculatePrice(Show show, List<Seat> seats);
}

public class CategoryPricing implements PricingStrategy {
    @Override
    public double calculatePrice(Show show, List<Seat> seats) {
        return seats.stream()
            .mapToDouble(seat -> show.getCategoryPrices()
                .getOrDefault(seat.getCategory(), 150.0))
            .sum();
    }
}

public class SurgePricing implements PricingStrategy {
    private final PricingStrategy basePricing = new CategoryPricing();

    @Override
    public double calculatePrice(Show show, List<Seat> seats) {
        double base = basePricing.calculatePrice(show, seats);
        double occupancyRate = show.getOccupancyRate(); // booked / total
        double multiplier = occupancyRate > 0.8 ? 1.5 : (occupancyRate > 0.6 ? 1.2 : 1.0);
        return base * multiplier;
    }
}

BookingService — Full Booking Flow

public class BookingService {
    private final SeatLockService lockService;
    private final PricingStrategy pricingStrategy;
    private final PaymentService paymentService;
    private final BookingRepository bookingRepo;

    public Booking initiateBooking(String showId, List<String> seatIds, String userId) {
        boolean locked = lockService.lockSeats(showId, seatIds, userId);
        if (!locked) throw new SeatsUnavailableException("One or more seats already locked");

        Show show = showRepo.findById(showId);
        List<Seat> seats = show.getSeats(seatIds);
        double amount = pricingStrategy.calculatePrice(show, seats);

        Booking booking = new Booking(UUID.randomUUID().toString(),
            show, userRepo.findById(userId), seats, amount, BookingStatus.PENDING);
        return bookingRepo.save(booking);
    }

    public Booking confirmBooking(String bookingId, PaymentRequest paymentReq) {
        Booking booking = bookingRepo.findById(bookingId);
        if (booking.getStatus() != BookingStatus.PENDING)
            throw new InvalidBookingStateException("Booking is not pending");

        // Verify locks are still valid (not expired)
        String showId = booking.getShow().getId();
        List<String> seatIds = booking.getSeatIds();
        if (!lockService.areLocksValid(showId, seatIds, booking.getUserId()))
            throw new SeatLockExpiredException("Seat locks have expired — please re-select");

        Payment payment = paymentService.charge(paymentReq, booking.getTotalAmount());
        if (payment.getStatus() != PaymentStatus.SUCCESS) {
            lockService.releaseLocks(showId, seatIds);
            booking.setStatus(BookingStatus.PAYMENT_FAILED);
            return bookingRepo.save(booking);
        }

        // Mark seats as permanently booked
        booking.getSeats().forEach(seat -> seat.setStatus(SeatStatus.BOOKED));
        booking.setStatus(BookingStatus.CONFIRMED);
        booking.setPaymentId(payment.getId());
        lockService.releaseLocks(showId, seatIds); // remove temp lock (seat is now BOOKED)
        return bookingRepo.save(booking);
    }

    public void cancelBooking(String bookingId) {
        Booking booking = bookingRepo.findById(bookingId);
        if (booking.getStatus() != BookingStatus.CONFIRMED)
            throw new InvalidBookingStateException("Only confirmed bookings can be cancelled");
        booking.getSeats().forEach(seat -> seat.setStatus(SeatStatus.AVAILABLE));
        booking.setStatus(BookingStatus.CANCELLED);
        paymentService.refund(booking.getPaymentId());
        bookingRepo.save(booking);
    }
}

Key Design Decisions

  • Per-show lock instead of global lock: A single global lock serializes all seat bookings across all shows. A per-show ReentrantLock limits contention to users booking the same show — unrelated shows run concurrently.
  • Expiry check inside the lock: Checking lock expiry outside the lock creates a TOCTOU race — another thread can steal the seat between the check and the actual locking. Both operations happen inside the same lock acquisition.
  • Booking state machine (PENDING → CONFIRMED / CANCELLED): Separating booking creation from payment confirmation means a failed payment has a clean state to transition to, and re-attempts are explicit rather than implicit.
  • SeatLock entity vs boolean flag: A dedicated SeatLock with userId and expiresAt lets you answer "is this lock mine?" and "has it expired?" in O(1) without a background thread scanning for expired locks.

Common Follow-Up Questions

  • "How do you auto-expire seat locks after 10 minutes?" — Each SeatLock stores expiresAt. The isExpired() method checks LocalDateTime.now() against expiresAt. No background thread needed — expiry is lazy (checked on next access). Optionally, use a scheduled executor to clean up stale entries from the map.
  • "How do you scale this to handle 100,000 concurrent bookings for a blockbuster?"— Shard shows across application servers. Each show is owned by one shard (consistent hashing on showId). The per-show lock remains local. Use Redis SETNX with a TTL as the distributed seat lock if you need multi-node safety.
  • "How do you display a live seat map?" — Serve seat status from a Redis sorted set or an in-memory cache. On seat selection, update Redis atomically (SETNX with TTL). WebSocket pushes seat status changes to all users viewing the same show.

FAQ — Movie Ticket Booking Low Level Design

What design patterns are used in BookMyShow LLD?

The primary patterns are Strategy (CategoryPricing vs SurgePricing),State Machine (booking lifecycle: PENDING → CONFIRMED → CANCELLED), andRepository (BookingRepository, ShowRepository). The seat locking mechanism uses a per-entity lock pattern inspired by fine-grained locking.

How do you prevent double booking in a movie ticket system?

Use a per-show ReentrantLock. Before marking any seat as locked, acquire the show-level lock, verify all selected seats are free (not locked or booked), then lock them atomically. Release the Java lock immediately after — the SeatLock entity holds the logical reservation for 10 minutes. At the DB layer, add a unique constraint on (showId, seatId, status=BOOKED) as a safety net.

What is the difference between a seat lock and a booking?

A seat lock is a temporary reservation (10 minutes) created when a user selects seats but before payment. A booking is a permanent record created the moment the user initiates checkout. The booking starts in PENDING state. After successful payment, both the booking status and seat status are updated to CONFIRMED/BOOKED in the same transaction.

How do you model dynamic pricing for peak shows?

Implement SurgePricing as a Strategy that delegates to CategoryPricing for the base price, then applies a multiplier based on the show's current occupancy rate. The occupancy rate is a cheap derived value: (totalSeats - availableSeats) / totalSeats. No separate price table per time window is needed.

Ready to practice?

Submit your solution and get AI-scored feedback on OOP, SOLID principles, design patterns, and code quality.

Solve Movie Ticket Booking (BookMyShow)