LLD Problem

Design BookMyShow

Design a movie ticket booking system with theaters, shows, seat selection, and concurrent booking handling.

25 min readConcurrency Focus

1Requirements

Functional
  • List movies, theaters, shows
  • Search by city, date, movie
  • View seat availability
  • Select and book seats
  • Process payment
  • Send booking confirmation
Non-Functional
  • Handle concurrent bookings
  • Seat lock with timeout (10 min)
  • No double booking
  • Dynamic pricing support
  • High availability

2Class Design

Movie
- id
- title
- duration
- genre
- rating
+ getShows(city)
Theater
- id
- name
- city
- screens[]
+ getShows(movie)
Screen
- id
- theater
- seats[]
- shows[]
+ getLayout()
Show
- id
- movie
- screen
- startTime
- pricing
+ getAvailableSeats()
Seat
- id
- row
- number
- type
+ isAvailable(show)
Booking
- id
- user
- show
- seats[]
- status
+ confirm()
+ cancel()
Key Insight: Seat availability is per show, not per seat. A seat can be booked for one show but available for another. Use a ShowSeatjunction class or a separate availability table.

3Handling Concurrent Bookings

The biggest challenge is preventing double-booking when multiple users try to book the same seats simultaneously.

Pessimistic Locking

Lock seats when user selects them.

  • + Guarantees no conflicts
  • - Can cause deadlocks
  • - Poor user experience if lock held long
Optimistic Locking (Preferred)

Check and book atomically at payment time.

  • + Better user experience
  • + No deadlocks
  • - May fail at last step

Recommended Booking Flow

1. User selects seats
   → Mark seats as TEMPORARILY_LOCKED (10 min timeout)
   → Store lock with userId and expiry

2. User proceeds to payment
   → Validate seats still locked by this user
   → If expired, show error

3. Payment processing
   → Start database transaction
   → Check seat status = LOCKED by this user
   → Process payment
   → Update seat status to BOOKED
   → Commit transaction

4. Background job
   → Every minute, release expired locks
   → Set status back to AVAILABLE

// Atomic check-and-book query:
UPDATE show_seats 
SET status = 'BOOKED', booking_id = ?
WHERE show_id = ? AND seat_id IN (...)
  AND status = 'LOCKED'
  AND locked_by = ?
  AND lock_expiry > NOW()

4Seat Layout Representation

Sample Theater Layout

SCREEN
A
1
2
3
4
5
6
7
8
9
10
B
1
2
3
4
5
6
7
8
9
10
C
1
2
3
4
5
6
7
8
9
10
D
1
2
3
4
5
6
7
8
9
10
E
1
2
3
4
5
6
7
8
9
10
Available
Booked
Selected

5Implementation

The key is managing seat state transitions: AVAILABLE → LOCKED → BOOKED (or back to AVAILABLE on timeout).

Movie Ticket Booking System
// ========== Enums ==========
enum SeatStatus { AVAILABLE, LOCKED, BOOKED }
enum SeatType { REGULAR, PREMIUM, VIP }
enum BookingStatus { PENDING, CONFIRMED, CANCELLED }

// ========== Movie ==========
class Movie {
    private String id;
    private String title;
    private int durationMinutes;
    private String genre;
    
    public Movie(String id, String title, int duration, String genre) {
        this.id = id;
        this.title = title;
        this.durationMinutes = duration;
        this.genre = genre;
    }
    
    public String getId() { return id; }
    public String getTitle() { return title; }
}

// ========== Seat ==========
class Seat {
    private String id;
    private String row;
    private int number;
    private SeatType type;
    private double basePrice;
    
    public Seat(String row, int number, SeatType type, double price) {
        this.id = row + number;
        this.row = row;
        this.number = number;
        this.type = type;
        this.basePrice = price;
    }
    
    public String getId() { return id; }
    public double getBasePrice() { return basePrice; }
}

// ========== ShowSeat (Junction) ==========
class ShowSeat {
    private Show show;
    private Seat seat;
    private SeatStatus status;
    private String lockedBy;
    private LocalDateTime lockExpiry;
    private Booking booking;
    
    public ShowSeat(Show show, Seat seat) {
        this.show = show;
        this.seat = seat;
        this.status = SeatStatus.AVAILABLE;
    }
    
    public synchronized boolean lock(String userId, int lockMinutes) {
        if (status != SeatStatus.AVAILABLE) return false;
        this.status = SeatStatus.LOCKED;
        this.lockedBy = userId;
        this.lockExpiry = LocalDateTime.now().plusMinutes(lockMinutes);
        return true;
    }
    
    public synchronized boolean book(String userId, Booking booking) {
        if (status != SeatStatus.LOCKED || !lockedBy.equals(userId)) {
            return false;
        }
        if (LocalDateTime.now().isAfter(lockExpiry)) {
            unlock();
            return false;
        }
        this.status = SeatStatus.BOOKED;
        this.booking = booking;
        return true;
    }
    
    public synchronized void unlock() {
        if (status == SeatStatus.LOCKED) {
            this.status = SeatStatus.AVAILABLE;
            this.lockedBy = null;
            this.lockExpiry = null;
        }
    }
    
    public boolean isAvailable() { return status == SeatStatus.AVAILABLE; }
    public boolean isExpired() { 
        return status == SeatStatus.LOCKED && 
               LocalDateTime.now().isAfter(lockExpiry); 
    }
    public Seat getSeat() { return seat; }
}

// ========== Show ==========
class Show {
    private String id;
    private Movie movie;
    private Screen screen;
    private LocalDateTime startTime;
    private Map<String, ShowSeat> showSeats; // seatId -> ShowSeat
    
    public Show(String id, Movie movie, Screen screen, LocalDateTime time) {
        this.id = id;
        this.movie = movie;
        this.screen = screen;
        this.startTime = time;
        this.showSeats = new ConcurrentHashMap<>();
        
        // Initialize ShowSeat for each seat in screen
        for (Seat seat : screen.getSeats()) {
            showSeats.put(seat.getId(), new ShowSeat(this, seat));
        }
    }
    
    public List<ShowSeat> getAvailableSeats() {
        return showSeats.values().stream()
            .filter(ShowSeat::isAvailable)
            .collect(Collectors.toList());
    }
    
    public ShowSeat getShowSeat(String seatId) {
        return showSeats.get(seatId);
    }
    
    public String getId() { return id; }
    public Movie getMovie() { return movie; }
}

// ========== Screen ==========
class Screen {
    private String id;
    private Theater theater;
    private List<Seat> seats;
    
    public Screen(String id, Theater theater) {
        this.id = id;
        this.theater = theater;
        this.seats = new ArrayList<>();
    }
    
    public void addSeat(Seat seat) { seats.add(seat); }
    public List<Seat> getSeats() { return seats; }
}

// ========== Theater ==========
class Theater {
    private String id;
    private String name;
    private String city;
    private List<Screen> screens;
    
    public Theater(String id, String name, String city) {
        this.id = id;
        this.name = name;
        this.city = city;
        this.screens = new ArrayList<>();
    }
    
    public void addScreen(Screen screen) { screens.add(screen); }
}

// ========== Booking ==========
class Booking {
    private String id;
    private String oderId;
    private Show show;
    private List<ShowSeat> seats;
    private String userId;
    private BookingStatus status;
    private double totalAmount;
    private LocalDateTime createdAt;
    
    public Booking(String userId, Show show, List<ShowSeat> seats) {
        this.id = UUID.randomUUID().toString().substring(0, 8);
        this.userId = userId;
        this.show = show;
        this.seats = seats;
        this.status = BookingStatus.PENDING;
        this.createdAt = LocalDateTime.now();
        this.totalAmount = calculateTotal();
    }
    
    private double calculateTotal() {
        return seats.stream()
            .mapToDouble(ss -> ss.getSeat().getBasePrice())
            .sum();
    }
    
    public boolean confirm() {
        // Atomically book all seats
        for (ShowSeat ss : seats) {
            if (!ss.book(userId, this)) {
                // Rollback: unlock already booked
                cancel();
                return false;
            }
        }
        this.status = BookingStatus.CONFIRMED;
        return true;
    }
    
    public void cancel() {
        for (ShowSeat ss : seats) {
            ss.unlock();
        }
        this.status = BookingStatus.CANCELLED;
    }
    
    public String getId() { return id; }
    public BookingStatus getStatus() { return status; }
    public double getTotalAmount() { return totalAmount; }
}

// ========== BookingService ==========
class BookingService {
    private static final int LOCK_TIMEOUT_MINUTES = 10;
    private Map<String, Booking> bookings = new ConcurrentHashMap<>();
    
    public List<ShowSeat> lockSeats(String userId, Show show, List<String> seatIds) {
        List<ShowSeat> lockedSeats = new ArrayList<>();
        
        for (String seatId : seatIds) {
            ShowSeat showSeat = show.getShowSeat(seatId);
            if (showSeat == null || !showSeat.lock(userId, LOCK_TIMEOUT_MINUTES)) {
                // Rollback: unlock already locked
                lockedSeats.forEach(ShowSeat::unlock);
                throw new RuntimeException("Seat " + seatId + " unavailable");
            }
            lockedSeats.add(showSeat);
        }
        
        return lockedSeats;
    }
    
    public Booking createBooking(String userId, Show show, List<ShowSeat> seats) {
        Booking booking = new Booking(userId, show, seats);
        bookings.put(booking.getId(), booking);
        return booking;
    }
    
    public boolean confirmBooking(String bookingId, Payment payment) {
        Booking booking = bookings.get(bookingId);
        if (booking == null) return false;
        
        // Process payment first
        if (!payment.process()) {
            booking.cancel();
            return false;
        }
        
        // Then confirm booking
        return booking.confirm();
    }
}

// ========== Usage Demo ==========
public class BookMyShowDemo {
    public static void main(String[] args) {
        // Setup
        Theater theater = new Theater("T1", "PVR", "Mumbai");
        Screen screen = new Screen("S1", theater);
        for (char row = 'A'; row <= 'E'; row++) {
            for (int i = 1; i <= 10; i++) {
                SeatType type = row <= 'B' ? SeatType.VIP : SeatType.REGULAR;
                double price = type == SeatType.VIP ? 500 : 300;
                screen.addSeat(new Seat(String.valueOf(row), i, type, price));
            }
        }
        
        Movie movie = new Movie("M1", "Inception", 150, "Sci-Fi");
        Show show = new Show("SH1", movie, screen, LocalDateTime.now().plusDays(1));
        
        // Booking flow
        BookingService service = new BookingService();
        String userId = "user123";
        
        try {
            // 1. Lock seats
            List<ShowSeat> locked = service.lockSeats(userId, show, 
                Arrays.asList("A1", "A2"));
            System.out.println("Seats locked!");
            
            // 2. Create booking
            Booking booking = service.createBooking(userId, show, locked);
            System.out.println("Booking: " + booking.getId());
            System.out.println("Total: Rs " + booking.getTotalAmount());
            
            // 3. Process payment & confirm
            Payment payment = new Payment(booking.getTotalAmount());
            if (service.confirmBooking(booking.getId(), payment)) {
                System.out.println("Booking confirmed!");
            }
        } catch (Exception e) {
            System.out.println("Failed: " + e.getMessage());
        }
    }
}

6Interview Follow-up Questions

Interview Follow-up Questions

Common follow-up questions interviewers ask

6Key Takeaways

1Seat availability is per show, not per seat.
2Use temporary locks with timeout for seat selection.
3Atomic database transactions for final booking.
4Background job to release expired locks.
5Strategy pattern for dynamic pricing.
6Handle concurrency at both application and database level.