Skip to content
1 min read

Design a Movie Ticket Booking System

Interview Context

Asked at: Amazon, Microsoft, Walmart, Uber | Level: L4-L6 | Time: 45 minutes | Type: LLD/OOP Design


Requirements

Functional

  • Browse movies, theaters, and showtimes
  • View seat map with real-time availability (available/locked/booked)
  • Select seats — temporarily lock them for the user (5 min timeout)
  • Complete payment within timeout to confirm booking
  • Release locked seats automatically on timeout or cancellation
  • Different pricing for seat categories (regular, premium, VIP)

Non-Functional

  • No double-booking (two users cannot book the same seat)
  • Handle concurrent seat selection (optimistic locking)
  • Payment timeout releases seats back to available pool
  • Support 1000+ concurrent users per show

Class Diagram

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
classDiagram
    class BookingService {
        -SeatLockManager lockManager
        -PaymentProcessor paymentProcessor
        +selectSeats(User, Show, List~Seat~) SeatLock
        +confirmBooking(SeatLock, PaymentMethod) Booking
        +cancelBooking(SeatLock) void
    }

    class Theater {
        -String id
        -String name
        -List~Screen~ screens
    }

    class Screen {
        -int screenId
        -List~Seat~ seats
        -int capacity
    }

    class Show {
        -String showId
        -Movie movie
        -Screen screen
        -LocalDateTime startTime
        -Map~Seat, SeatStatus~ seatMap
    }

    class Seat {
        -String seatId
        -int row
        -int col
        -SeatCategory category
    }

    class SeatLock {
        -String lockId
        -User user
        -Show show
        -List~Seat~ seats
        -LocalDateTime expiresAt
        -LockStatus status
    }

    class Booking {
        -String bookingId
        -User user
        -Show show
        -List~Seat~ seats
        -double totalAmount
        -BookingStatus status
    }

    class SeatCategory {
        <<enumeration>>
        REGULAR
        PREMIUM
        VIP
    }

    class SeatStatus {
        <<enumeration>>
        AVAILABLE
        LOCKED
        BOOKED
    }

    class PricingStrategy {
        <<interface>>
        +calculatePrice(Seat, Show) double
    }

    Theater "1" --> "*" Screen
    Screen "1" --> "*" Seat
    Show --> Screen
    Show --> Movie
    SeatLock --> Show
    SeatLock --> Seat
    Booking --> Show
    Booking --> Seat
    BookingService --> SeatLockManager
    Seat --> SeatCategory

Key Design Decisions

Decision Choice Why
Seat locking Temporary lock with TTL Prevents double-booking during payment
Concurrency CAS on seat status Multiple users selecting same seats simultaneously
Lock expiry Scheduled cleanup thread Releases abandoned locks automatically
Pricing Strategy Pattern Dynamic pricing, category-based, time-of-day
Seat state State Machine AVAILABLE → LOCKED → BOOKED (or back to AVAILABLE)

Java Implementation

Java
public class SeatLockManager {
    private final ConcurrentHashMap<String, SeatLock> activeLocks = new ConcurrentHashMap<>();
    private final Duration lockTimeout;
    private final ScheduledExecutorService scheduler;

    public SeatLockManager(Duration lockTimeout) {
        this.lockTimeout = lockTimeout;
        this.scheduler = Executors.newScheduledThreadPool(1);
        // Periodic cleanup of expired locks
        scheduler.scheduleAtFixedRate(this::cleanupExpired, 30, 30, TimeUnit.SECONDS);
    }

    public SeatLock lockSeats(User user, Show show, List<Seat> seats) {
        // Attempt atomic lock on all seats
        synchronized (show) {
            for (Seat seat : seats) {
                SeatStatus status = show.getSeatStatus(seat);
                if (status != SeatStatus.AVAILABLE) {
                    throw new SeatUnavailableException(seat);
                }
            }
            // All available — lock them atomically
            seats.forEach(s -> show.setSeatStatus(s, SeatStatus.LOCKED));
        }

        SeatLock lock = new SeatLock(
            UUID.randomUUID().toString(), user, show, seats,
            LocalDateTime.now().plus(lockTimeout)
        );
        activeLocks.put(lock.getLockId(), lock);
        return lock;
    }

    public void releaseLock(SeatLock lock) {
        synchronized (lock.getShow()) {
            lock.getSeats().forEach(s -> lock.getShow().setSeatStatus(s, SeatStatus.AVAILABLE));
        }
        lock.setStatus(LockStatus.EXPIRED);
        activeLocks.remove(lock.getLockId());
    }

    private void cleanupExpired() {
        LocalDateTime now = LocalDateTime.now();
        activeLocks.values().stream()
            .filter(lock -> lock.getExpiresAt().isBefore(now))
            .forEach(this::releaseLock);
    }
}
Java
public class BookingService {
    private final SeatLockManager lockManager;
    private final PaymentProcessor paymentProcessor;
    private final PricingStrategy pricingStrategy;
    private final Map<String, Booking> bookings = new ConcurrentHashMap<>();

    public SeatLock selectSeats(User user, Show show, List<Seat> seats) {
        return lockManager.lockSeats(user, show, seats);
    }

    public Booking confirmBooking(SeatLock lock, PaymentMethod method) {
        // Verify lock still valid
        if (lock.isExpired()) {
            throw new LockExpiredException("Seat lock expired. Please re-select seats.");
        }

        // Calculate total
        double total = lock.getSeats().stream()
            .mapToDouble(seat -> pricingStrategy.calculatePrice(seat, lock.getShow()))
            .sum();

        // Process payment
        PaymentResult result = paymentProcessor.process(total, method);
        if (!result.isSuccess()) {
            lockManager.releaseLock(lock);
            throw new PaymentFailedException(result.getReason());
        }

        // Confirm booking — transition seats to BOOKED
        synchronized (lock.getShow()) {
            lock.getSeats().forEach(s -> lock.getShow().setSeatStatus(s, SeatStatus.BOOKED));
        }
        lock.setStatus(LockStatus.CONFIRMED);

        Booking booking = new Booking(
            UUID.randomUUID().toString(), lock.getUser(), lock.getShow(),
            lock.getSeats(), total, BookingStatus.CONFIRMED
        );
        bookings.put(booking.getBookingId(), booking);
        return booking;
    }

    public void cancelBooking(String bookingId) {
        Booking booking = bookings.get(bookingId);
        if (booking == null) throw new BookingNotFoundException(bookingId);

        synchronized (booking.getShow()) {
            booking.getSeats().forEach(s -> booking.getShow().setSeatStatus(s, SeatStatus.AVAILABLE));
        }
        booking.setStatus(BookingStatus.CANCELLED);
        // Trigger refund asynchronously
        paymentProcessor.refundAsync(booking);
    }
}
Java
public interface PricingStrategy {
    double calculatePrice(Seat seat, Show show);
}

public class CategoryBasedPricing implements PricingStrategy {
    private static final Map<SeatCategory, Double> BASE_PRICES = Map.of(
        SeatCategory.REGULAR, 10.0,
        SeatCategory.PREMIUM, 18.0,
        SeatCategory.VIP, 30.0
    );

    @Override
    public double calculatePrice(Seat seat, Show show) {
        double base = BASE_PRICES.get(seat.getCategory());
        // Weekend surcharge
        DayOfWeek day = show.getStartTime().getDayOfWeek();
        if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) {
            base *= 1.25;
        }
        // Prime time surcharge (6PM-9PM)
        int hour = show.getStartTime().getHour();
        if (hour >= 18 && hour <= 21) {
            base *= 1.15;
        }
        return base;
    }
}

public class DemandBasedPricing implements PricingStrategy {
    @Override
    public double calculatePrice(Seat seat, Show show) {
        double base = new CategoryBasedPricing().calculatePrice(seat, show);
        double occupancy = show.getOccupancyRatio();
        if (occupancy > 0.75) base *= 1.3; // high demand
        return base;
    }
}

SOLID Principles Applied

Principle How Applied
S — Single Responsibility SeatLockManager handles locking/expiry; BookingService orchestrates flow
O — Open/Closed New pricing strategies added without modifying booking logic
L — Liskov Substitution CategoryBasedPricing and DemandBasedPricing are interchangeable
I — Interface Segregation PricingStrategy has one method; PaymentProcessor is separate
D — Dependency Inversion BookingService depends on interfaces for pricing and payment

Interview Walkthrough (45 minutes)

Time What to Do
0-5 min Clarify: single theater vs. chain, seat categories, lock duration, payment modes
5-15 min Draw class diagram — Show, Seat (state machine), SeatLock, Booking, PricingStrategy
15-25 min Explain seat locking: temporary lock, TTL expiry, concurrent selection handling
25-35 min Code: SeatLockManager.lockSeats(), BookingService.confirmBooking()
35-45 min Discuss: bulk booking, waitlist, refund policy, notifications on lock expiry