Code · LLD machine coding · PhonePe prep

BookMyShow

Design the backend for booking movie tickets.

37 classes 1016 lines StrategyFactoryRepositoryLockingCustom exceptionsService layer View on GitHub ↗

Question

Design the backend for booking movie tickets.

  • Admins add movies, theatres and screens (seat rows with a type: normal, premium, recliner) and create shows.
  • Users search shows for a movie in a city and see seat availability.
  • A user picks seats. The seats are locked for a short time (30s here) while they pay, and other users can’t take them.
  • Pay by card or UPI. On success the booking is confirmed; if the lock expires, the seats are released.
  • Price depends on seat type.
  • Concurrency: many users racing for the same seat; exactly one may win.

Design: repositories + services, SeatLockManager (per-seat locks with expiry), Strategy for pricing and payment, a factory for payment methods, and custom exceptions. Run BookMyShowDemo (includes a 10-thread race for one seat).

entities/Booking.java 47 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.entities;

import java.util.List;
import java.util.UUID;

public class Booking {

    private final String id;
    private final String userId;
    private final String showId;
    private final List<String> seatIds;
    private final double totalAmount;
    private BookingStatus status;
    private Payment payment;

    public Booking(String userId, String showId, List<String> seatIds, double totalAmount) {
        this.id = UUID.randomUUID().toString();
        this.userId = userId;
        this.showId = showId;
        this.seatIds = seatIds;
        this.totalAmount = totalAmount;
        this.status = BookingStatus.CREATED;
    }

    public String getId() {
        return id;
    }

    public String getUserId() {
        return userId;
    }

    public String getShowId() {
        return showId;
    }

    public List<String> getSeatIds() {
        return seatIds;
    }

    public double getTotalAmount() {
        return totalAmount;
    }

    public BookingStatus getStatus() {
        return status;
    }

    public void setStatus(BookingStatus status) {
        this.status = status;
    }

    public Payment getPayment() {
        return payment;
    }

    public void setPayment(Payment payment) {
        this.payment = payment;
    }
}
entities/BookingStatus.java 7 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.entities;

public enum BookingStatus {
    CREATED,
    CONFIRMED,
    CANCELLED,
    EXPIRED
}
entities/Movie.java 47 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.entities;

import java.util.UUID;

public class Movie {

    private final String id;
    private String title;
    private String language;
    private String genre;
    private int durationMinutes;

    public Movie(String title, String language, String genre, int durationMinutes) {
        this.id = UUID.randomUUID().toString();
        this.title = title;
        this.language = language;
        this.genre = genre;
        this.durationMinutes = durationMinutes;
    }

    public String getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getLanguage() {
        return language;
    }

    public void setLanguage(String language) {
        this.language = language;
    }

    public String getGenre() {
        return genre;
    }

    public void setGenre(String genre) {
        this.genre = genre;
    }

    public int getDurationMinutes() {
        return durationMinutes;
    }

    public void setDurationMinutes(int durationMinutes) {
        this.durationMinutes = durationMinutes;
    }

    @Override
    public String toString() {
        return title + " (" + language + ", " + genre + ")";
    }
}
entities/Payment.java 34 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.entities;

import java.util.UUID;

public class Payment {

    private final String id;
    private final String bookingId;
    private final double amount;
    private final String method;
    private PaymentStatus status;

    public Payment(String bookingId, double amount, String method, PaymentStatus status) {
        this.id = UUID.randomUUID().toString();
        this.bookingId = bookingId;
        this.amount = amount;
        this.method = method;
        this.status = status;
    }

    public String getId() {
        return id;
    }

    public String getBookingId() {
        return bookingId;
    }

    public double getAmount() {
        return amount;
    }

    public String getMethod() {
        return method;
    }

    public PaymentStatus getStatus() {
        return status;
    }

    public void setStatus(PaymentStatus status) {
        this.status = status;
    }
}
entities/PaymentStatus.java 6 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.entities;

public enum PaymentStatus {
    PENDING,
    SUCCESS,
    FAILED
}
entities/Screen.java 25 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.entities;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class Screen {

    private final String id;
    private final String name;
    private final List<Seat> seats = new ArrayList<>();

    public Screen(String name) {
        this.id = UUID.randomUUID().toString();
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<Seat> getSeats() {
        return seats;
    }

    public void addSeat(Seat seat) {
        seats.add(seat);
    }
}
entities/Seat.java 29 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.entities;

import java.util.UUID;

public class Seat {

    private final String id;
    private final String row;
    private final int seatNumber;
    private final SeatType seatType;

    public Seat(String row, int seatNumber, SeatType seatType) {
        this.id = UUID.randomUUID().toString();
        this.row = row;
        this.seatNumber = seatNumber;
        this.seatType = seatType;
    }

    public String getId() {
        return id;
    }

    public String getRow() {
        return row;
    }

    public int getSeatNumber() {
        return seatNumber;
    }

    public SeatType getSeatType() {
        return seatType;
    }

    public String getLabel() {
        return row + seatNumber;
    }
}
entities/SeatStatus.java 6 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.entities;

public enum SeatStatus {
    AVAILABLE,
    LOCKED,
    BOOKED
}
entities/SeatType.java 6 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.entities;

public enum SeatType {
    NORMAL,
    PREMIUM,
    RECLINER
}
entities/Show.java 46 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.entities;

import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;

public class Show {

    private final String id;
    private final String movieId;
    private final String theatreId;
    private final String screenId;
    private final LocalDateTime startTime;
    private final LocalDateTime endTime;
    private final Map<String, ShowSeat> showSeatsBySeatId = new LinkedHashMap<>();

    public Show(String movieId, String theatreId, String screenId, LocalDateTime startTime, LocalDateTime endTime) {
        this.id = UUID.randomUUID().toString();
        this.movieId = movieId;
        this.theatreId = theatreId;
        this.screenId = screenId;
        this.startTime = startTime;
        this.endTime = endTime;
    }

    public String getId() {
        return id;
    }

    public String getMovieId() {
        return movieId;
    }

    public String getTheatreId() {
        return theatreId;
    }

    public String getScreenId() {
        return screenId;
    }

    public LocalDateTime getStartTime() {
        return startTime;
    }

    public LocalDateTime getEndTime() {
        return endTime;
    }

    public Map<String, ShowSeat> getShowSeatsBySeatId() {
        return showSeatsBySeatId;
    }

    public void addShowSeat(ShowSeat showSeat) {
        showSeatsBySeatId.put(showSeat.getSeat().getId(), showSeat);
    }
}
entities/ShowSeat.java 73 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.entities;

/**
 * Per-show mutable booking state for a physical {@link Seat}. The same seat is reused across
 * many shows, so availability/locking is tracked here instead of on the seat itself.
 */
public class ShowSeat {

    private final String showId;
    private final Seat seat;
    private final double price;

    private SeatStatus status;
    private String lockedByUserId;
    private long lockExpiryEpochMillis;

    public ShowSeat(String showId, Seat seat, double price) {
        this.showId = showId;
        this.seat = seat;
        this.price = price;
        this.status = SeatStatus.AVAILABLE;
    }

    public String getId() {
        return showId + "_" + seat.getId();
    }

    public String getShowId() {
        return showId;
    }

    public Seat getSeat() {
        return seat;
    }

    public double getPrice() {
        return price;
    }

    public synchronized SeatStatus getStatus() {
        return status;
    }

    public synchronized String getLockedByUserId() {
        return lockedByUserId;
    }

    /** Attempts to move AVAILABLE -> LOCKED for the given user. Self-heals stale locks that outlived their TTL. */
    public synchronized boolean tryLock(String userId, long ttlMillis, long nowEpochMillis) {
        expireIfNeeded(nowEpochMillis);
        if (status != SeatStatus.AVAILABLE) {
            return false;
        }
        status = SeatStatus.LOCKED;
        lockedByUserId = userId;
        lockExpiryEpochMillis = nowEpochMillis + ttlMillis;
        return true;
    }

    /** Moves LOCKED -> BOOKED, only if still held by the same user and not expired. */
    public synchronized boolean confirmBooking(String userId, long nowEpochMillis) {
        expireIfNeeded(nowEpochMillis);
        if (status != SeatStatus.LOCKED || !lockedByUserId.equals(userId)) {
            return false;
        }
        status = SeatStatus.BOOKED;
        return true;
    }

    /** Releases a LOCKED seat held by the given user back to AVAILABLE (cancellation / payment failure). */
    public synchronized void release(String userId) {
        if (status == SeatStatus.LOCKED && userId.equals(lockedByUserId)) {
            status = SeatStatus.AVAILABLE;
            lockedByUserId = null;
        }
    }

    /** Called by the background sweep; releases an expired lock back to AVAILABLE. Returns true if it did. */
    public synchronized boolean expireIfNeeded(long nowEpochMillis) {
        if (status == SeatStatus.LOCKED && nowEpochMillis >= lockExpiryEpochMillis) {
            status = SeatStatus.AVAILABLE;
            lockedByUserId = null;
            return true;
        }
        return false;
    }
}
entities/Theatre.java 30 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.entities;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class Theatre {

    private final String id;
    private final String name;
    private final String city;
    private final List<Screen> screens = new ArrayList<>();

    public Theatre(String name, String city) {
        this.id = UUID.randomUUID().toString();
        this.name = name;
        this.city = city;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getCity() {
        return city;
    }

    public List<Screen> getScreens() {
        return screens;
    }

    public void addScreen(Screen screen) {
        screens.add(screen);
    }
}
entities/User.java 21 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.entities;

import java.util.UUID;

public class User {

    private final String id;
    private final String name;
    private final String email;

    public User(String name, String email) {
        this.id = UUID.randomUUID().toString();
        this.name = name;
        this.email = email;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}
exception/EntityNotFoundException.java 6 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.exception;

public class EntityNotFoundException extends RuntimeException {
    public EntityNotFoundException(String message) {
        super(message);
    }
}
exception/InvalidSeatLockException.java 6 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.exception;

public class InvalidSeatLockException extends RuntimeException {
    public InvalidSeatLockException(String message) {
        super(message);
    }
}
exception/SeatUnavailableException.java 6 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.exception;

public class SeatUnavailableException extends RuntimeException {
    public SeatUnavailableException(String message) {
        super(message);
    }
}
lock/SeatLockManager.java 77 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.lock;

import com.practice.phonepe.questions.bookmyshow.entities.ShowSeat;
import com.practice.phonepe.questions.bookmyshow.exception.SeatUnavailableException;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Coordinates temporary seat holds while a user is checking out. A seat picked by one user is
 * LOCKED for a short TTL so nobody else can grab it; if checkout doesn't complete in time, a
 * background sweep releases it automatically so it becomes bookable again. This is what keeps
 * seat availability correct under concurrent bookings without holding the lock forever.
 */
public class SeatLockManager {

    private static final long DEFAULT_TTL_MILLIS = TimeUnit.MINUTES.toMillis(5);
    private static final long SWEEP_INTERVAL_MILLIS = TimeUnit.SECONDS.toMillis(1);

    private final long lockTtlMillis;
    private final ConcurrentHashMap<String, ShowSeat> activeLocks = new ConcurrentHashMap<>();
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
        Thread t = new Thread(r, "seat-lock-sweeper");
        t.setDaemon(true);
        return t;
    });

    public SeatLockManager() {
        this(DEFAULT_TTL_MILLIS);
    }

    public SeatLockManager(long lockTtlMillis) {
        this.lockTtlMillis = lockTtlMillis;
        scheduler.scheduleAtFixedRate(this::sweepExpiredLocks, SWEEP_INTERVAL_MILLIS, SWEEP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS);
    }

    /**
     * Locks every seat for the given user, or none at all. If any requested seat is already
     * taken, whatever was locked earlier in this call is rolled back before throwing.
     */
    public List<ShowSeat> lockSeats(String userId, List<ShowSeat> seats) {
        long now = System.currentTimeMillis();
        List<ShowSeat> locked = new ArrayList<>();
        for (ShowSeat seat : seats) {
            if (seat.tryLock(userId, lockTtlMillis, now)) {
                locked.add(seat);
                activeLocks.put(seat.getId(), seat);
            } else {
                releaseSeats(userId, locked);
                throw new SeatUnavailableException("Seat " + seat.getSeat().getLabel() + " is no longer available");
            }
        }
        return locked;
    }

    /** Moves held seats from LOCKED to BOOKED once payment succeeds. */
    public void confirmSeats(String userId, List<ShowSeat> seats) {
        long now = System.currentTimeMillis();
        for (ShowSeat seat : seats) {
            seat.confirmBooking(userId, now);
            activeLocks.remove(seat.getId());
        }
    }

    /** Releases held seats back to AVAILABLE, e.g. on cancellation or payment failure. */
    public void releaseSeats(String userId, List<ShowSeat> seats) {
        for (ShowSeat seat : seats) {
            seat.release(userId);
            activeLocks.remove(seat.getId());
        }
    }

    private void sweepExpiredLocks() {
        long now = System.currentTimeMillis();
        activeLocks.forEach((id, seat) -> {
            if (seat.expireIfNeeded(now)) {
                activeLocks.remove(id);
            }
        });
    }

    public void shutdown() {
        scheduler.shutdownNow();
    }
}
payment/CardPaymentStrategy.java 8 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.payment;

public class CardPaymentStrategy implements PaymentStrategy {

    @Override
    public boolean pay(String userId, double amount) {
        System.out.printf("Charging card for user %s: amount=%.2f%n", userId, amount);
        return true;
    }
}
payment/PaymentMethod.java 5 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.payment;

public enum PaymentMethod {
    CARD,
    UPI
}
payment/PaymentStrategy.java 4 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.payment;

public interface PaymentStrategy {
    boolean pay(String userId, double amount);
}
payment/PaymentStrategyFactory.java 24 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.payment;

import java.util.EnumMap;
import java.util.Map;

/**
 * Resolves the {@link PaymentStrategy} for a {@link PaymentMethod} so callers depend only on the
 * method the user picked, never on a concrete strategy class.
 */
public class PaymentStrategyFactory {

    private final Map<PaymentMethod, PaymentStrategy> strategiesByMethod = new EnumMap<>(PaymentMethod.class);

    public PaymentStrategyFactory() {
        strategiesByMethod.put(PaymentMethod.CARD, new CardPaymentStrategy());
        strategiesByMethod.put(PaymentMethod.UPI, new UpiPaymentStrategy());
    }

    public PaymentStrategy getStrategy(PaymentMethod method) {
        PaymentStrategy strategy = strategiesByMethod.get(method);
        if (strategy == null) {
            throw new IllegalArgumentException("No payment strategy registered for method: " + method);
        }
        return strategy;
    }

    public void registerStrategy(PaymentMethod method, PaymentStrategy strategy) {
        strategiesByMethod.put(method, strategy);
    }
}
payment/UpiPaymentStrategy.java 8 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.payment;

public class UpiPaymentStrategy implements PaymentStrategy {

    @Override
    public boolean pay(String userId, double amount) {
        System.out.printf("Charging UPI for user %s: amount=%.2f%n", userId, amount);
        return true;
    }
}
pricing/DefaultPricingStrategy.java 17 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.pricing;

import com.practice.phonepe.questions.bookmyshow.entities.SeatType;
import com.practice.phonepe.questions.bookmyshow.entities.Show;

import java.util.EnumMap;
import java.util.Map;

public class DefaultPricingStrategy implements PricingStrategy {

    private final Map<SeatType, Double> basePriceBySeatType = new EnumMap<>(SeatType.class);

    public DefaultPricingStrategy() {
        basePriceBySeatType.put(SeatType.NORMAL, 150.0);
        basePriceBySeatType.put(SeatType.PREMIUM, 250.0);
        basePriceBySeatType.put(SeatType.RECLINER, 400.0);
    }

    @Override
    public double getPrice(Show show, SeatType seatType) {
        return basePriceBySeatType.getOrDefault(seatType, basePriceBySeatType.get(SeatType.NORMAL));
    }
}
pricing/PricingStrategy.java 6 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.pricing;

import com.practice.phonepe.questions.bookmyshow.entities.SeatType;
import com.practice.phonepe.questions.bookmyshow.entities.Show;

public interface PricingStrategy {
    double getPrice(Show show, SeatType seatType);
}
repository/BookingRepository.java 9 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.repository;

import com.practice.phonepe.questions.bookmyshow.entities.Booking;

import java.util.List;
import java.util.Optional;

public interface BookingRepository {
    void save(Booking booking);
    Optional<Booking> findById(String bookingId);
    List<Booking> findByUser(String userId);
}
repository/InMemoryBookingRepository.java 24 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.repository;

import com.practice.phonepe.questions.bookmyshow.entities.Booking;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

public class InMemoryBookingRepository implements BookingRepository {

    private final Map<String, Booking> bookingsById = new LinkedHashMap<>();

    @Override
    public void save(Booking booking) {
        bookingsById.put(booking.getId(), booking);
    }

    @Override
    public Optional<Booking> findById(String bookingId) {
        return Optional.ofNullable(bookingsById.get(bookingId));
    }

    @Override
    public List<Booking> findByUser(String userId) {
        return bookingsById.values().stream()
                .filter(booking -> booking.getUserId().equals(userId))
                .collect(Collectors.toList());
    }
}
repository/InMemoryMovieRepository.java 26 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.repository;

import com.practice.phonepe.questions.bookmyshow.entities.Movie;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class InMemoryMovieRepository implements MovieRepository {

    private final Map<String, Movie> moviesById = new LinkedHashMap<>();

    @Override
    public void save(Movie movie) {
        moviesById.put(movie.getId(), movie);
    }

    @Override
    public Optional<Movie> findById(String movieId) {
        return Optional.ofNullable(moviesById.get(movieId));
    }

    @Override
    public List<Movie> findAll() {
        return new ArrayList<>(moviesById.values());
    }

    @Override
    public void deleteById(String movieId) {
        moviesById.remove(movieId);
    }
}
repository/InMemoryShowRepository.java 38 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.repository;

import com.practice.phonepe.questions.bookmyshow.entities.Show;
import com.practice.phonepe.questions.bookmyshow.entities.Theatre;
import com.practice.phonepe.questions.bookmyshow.repository.TheatreRepository;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

public class InMemoryShowRepository implements ShowRepository {

    private final Map<String, Show> showsById = new LinkedHashMap<>();
    private final TheatreRepository theatreRepository;

    public InMemoryShowRepository(TheatreRepository theatreRepository) {
        this.theatreRepository = theatreRepository;
    }

    @Override
    public void save(Show show) {
        showsById.put(show.getId(), show);
    }

    @Override
    public Optional<Show> findById(String showId) {
        return Optional.ofNullable(showsById.get(showId));
    }

    @Override
    public List<Show> findByMovieAndCity(String movieId, String city) {
        return showsById.values().stream()
                .filter(show -> show.getMovieId().equals(movieId))
                .filter(show -> theatreRepository.findById(show.getTheatreId())
                        .map(Theatre::getCity)
                        .map(theatreCity -> theatreCity.equalsIgnoreCase(city))
                        .orElse(false))
                .collect(Collectors.toList());
    }

    @Override
    public void deleteById(String showId) {
        showsById.remove(showId);
    }
}
repository/InMemoryTheatreRepository.java 29 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.repository;

import com.practice.phonepe.questions.bookmyshow.entities.Theatre;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

public class InMemoryTheatreRepository implements TheatreRepository {

    private final Map<String, Theatre> theatresById = new LinkedHashMap<>();

    @Override
    public void save(Theatre theatre) {
        theatresById.put(theatre.getId(), theatre);
    }

    @Override
    public Optional<Theatre> findById(String theatreId) {
        return Optional.ofNullable(theatresById.get(theatreId));
    }

    @Override
    public List<Theatre> findByCity(String city) {
        return theatresById.values().stream()
                .filter(theatre -> theatre.getCity().equalsIgnoreCase(city))
                .collect(Collectors.toList());
    }

    @Override
    public List<Theatre> findAll() {
        return new ArrayList<>(theatresById.values());
    }
}
repository/MovieRepository.java 10 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.repository;

import com.practice.phonepe.questions.bookmyshow.entities.Movie;

import java.util.List;
import java.util.Optional;

public interface MovieRepository {
    void save(Movie movie);
    Optional<Movie> findById(String movieId);
    List<Movie> findAll();
    void deleteById(String movieId);
}
repository/ShowRepository.java 10 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.repository;

import com.practice.phonepe.questions.bookmyshow.entities.Show;

import java.util.List;
import java.util.Optional;

public interface ShowRepository {
    void save(Show show);
    Optional<Show> findById(String showId);
    List<Show> findByMovieAndCity(String movieId, String city);
    void deleteById(String showId);
}
repository/TheatreRepository.java 10 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.repository;

import com.practice.phonepe.questions.bookmyshow.entities.Theatre;

import java.util.List;
import java.util.Optional;

public interface TheatreRepository {
    void save(Theatre theatre);
    Optional<Theatre> findById(String theatreId);
    List<Theatre> findByCity(String city);
    List<Theatre> findAll();
}
service/BookingService.java 99 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.service;

import com.practice.phonepe.questions.bookmyshow.entities.Booking;
import com.practice.phonepe.questions.bookmyshow.entities.BookingStatus;
import com.practice.phonepe.questions.bookmyshow.entities.Payment;
import com.practice.phonepe.questions.bookmyshow.entities.PaymentStatus;
import com.practice.phonepe.questions.bookmyshow.entities.Show;
import com.practice.phonepe.questions.bookmyshow.entities.ShowSeat;
import com.practice.phonepe.questions.bookmyshow.exception.EntityNotFoundException;
import com.practice.phonepe.questions.bookmyshow.exception.InvalidSeatLockException;
import com.practice.phonepe.questions.bookmyshow.lock.SeatLockManager;
import com.practice.phonepe.questions.bookmyshow.payment.PaymentMethod;
import com.practice.phonepe.questions.bookmyshow.payment.PaymentStrategy;
import com.practice.phonepe.questions.bookmyshow.payment.PaymentStrategyFactory;
import com.practice.phonepe.questions.bookmyshow.repository.BookingRepository;

import java.util.List;
import java.util.stream.Collectors;

/**
 * Orchestrates the booking flow: hold seats -> collect payment -> confirm or roll back.
 * Seat-level concurrency is delegated entirely to {@link SeatLockManager}; this class just
 * sequences the steps and keeps {@link Booking} state consistent with seat state.
 */
public class BookingService {

    private final BookingRepository bookingRepository;
    private final ShowService showService;
    private final SeatLockManager seatLockManager;
    private final PaymentStrategyFactory paymentStrategyFactory;

    public BookingService(BookingRepository bookingRepository, ShowService showService, SeatLockManager seatLockManager,
                           PaymentStrategyFactory paymentStrategyFactory) {
        this.bookingRepository = bookingRepository;
        this.showService = showService;
        this.seatLockManager = seatLockManager;
        this.paymentStrategyFactory = paymentStrategyFactory;
    }

    /** Step 1: hold the requested seats for this user and create a pending booking. */
    public Booking initiateBooking(String userId, String showId, List<String> seatIds) {
        Show show = showService.getShow(showId);
        List<ShowSeat> requestedSeats = seatIds.stream()
                .map(seatId -> {
                    ShowSeat showSeat = show.getShowSeatsBySeatId().get(seatId);
                    if (showSeat == null) {
                        throw new EntityNotFoundException("Seat not found on show: " + seatId);
                    }
                    return showSeat;
                })
                .collect(Collectors.toList());

        List<ShowSeat> lockedSeats = seatLockManager.lockSeats(userId, requestedSeats);
        double totalAmount = lockedSeats.stream().mapToDouble(ShowSeat::getPrice).sum();

        Booking booking = new Booking(userId, showId, seatIds, totalAmount);
        bookingRepository.save(booking);
        return booking;
    }

    /** Step 2: charge the user and, on success, permanently claim the seats. */
    public Booking confirmBooking(String bookingId, PaymentMethod paymentMethod) {
        Booking booking = getBooking(bookingId);
        if (booking.getStatus() != BookingStatus.CREATED) {
            throw new InvalidSeatLockException("Booking " + bookingId + " is not awaiting payment");
        }

        Show show = showService.getShow(booking.getShowId());
        List<ShowSeat> seats = booking.getSeatIds().stream()
                .map(seatId -> show.getShowSeatsBySeatId().get(seatId))
                .collect(Collectors.toList());

        PaymentStrategy paymentStrategy = paymentStrategyFactory.getStrategy(paymentMethod);
        boolean paymentSucceeded = paymentStrategy.pay(booking.getUserId(), booking.getTotalAmount());
        if (!paymentSucceeded) {
            seatLockManager.releaseSeats(booking.getUserId(), seats);
            booking.setStatus(BookingStatus.CANCELLED);
            booking.setPayment(new Payment(bookingId, booking.getTotalAmount(), paymentMethod.name(), PaymentStatus.FAILED));
            bookingRepository.save(booking);
            return booking;
        }

        boolean allConfirmed = seats.stream()
                .allMatch(seat -> seat.confirmBooking(booking.getUserId(), System.currentTimeMillis()));
        if (!allConfirmed) {
            // Hold expired before payment finished; refund path would go here in a real system.
            seatLockManager.releaseSeats(booking.getUserId(), seats);
            booking.setStatus(BookingStatus.EXPIRED);
            bookingRepository.save(booking);
            throw new InvalidSeatLockException("Seat lock expired before payment could be confirmed for booking " + bookingId);
        }

        booking.setStatus(BookingStatus.CONFIRMED);
        booking.setPayment(new Payment(bookingId, booking.getTotalAmount(), paymentMethod.name(), PaymentStatus.SUCCESS));
        bookingRepository.save(booking);
        return booking;
    }

    public void cancelBooking(String bookingId) {
        Booking booking = getBooking(bookingId);
        Show show = showService.getShow(booking.getShowId());
        List<ShowSeat> seats = booking.getSeatIds().stream()
                .map(seatId -> show.getShowSeatsBySeatId().get(seatId))
                .collect(Collectors.toList());
        seatLockManager.releaseSeats(booking.getUserId(), seats);
        booking.setStatus(BookingStatus.CANCELLED);
        bookingRepository.save(booking);
    }

    public Booking getBooking(String bookingId) {
        return bookingRepository.findById(bookingId)
                .orElseThrow(() -> new EntityNotFoundException("Booking not found: " + bookingId));
    }
}
service/MovieService.java 27 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.service;

import com.practice.phonepe.questions.bookmyshow.entities.Movie;
import com.practice.phonepe.questions.bookmyshow.exception.EntityNotFoundException;
import com.practice.phonepe.questions.bookmyshow.repository.MovieRepository;

import java.util.List;

/** Admin-facing CRUD for the movie catalog. */
public class MovieService {

    private final MovieRepository movieRepository;

    public MovieService(MovieRepository movieRepository) {
        this.movieRepository = movieRepository;
    }

    public Movie addMovie(String title, String language, String genre, int durationMinutes) {
        Movie movie = new Movie(title, language, genre, durationMinutes);
        movieRepository.save(movie);
        return movie;
    }

    public Movie getMovie(String movieId) {
        return movieRepository.findById(movieId)
                .orElseThrow(() -> new EntityNotFoundException("Movie not found: " + movieId));
    }

    public List<Movie> listMovies() {
        return movieRepository.findAll();
    }

    public void removeMovie(String movieId) {
        movieRepository.deleteById(movieId);
    }
}
service/ShowService.java 41 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.service;

import com.practice.phonepe.questions.bookmyshow.entities.Screen;
import com.practice.phonepe.questions.bookmyshow.entities.Show;
import com.practice.phonepe.questions.bookmyshow.entities.ShowSeat;
import com.practice.phonepe.questions.bookmyshow.exception.EntityNotFoundException;
import com.practice.phonepe.questions.bookmyshow.pricing.PricingStrategy;
import com.practice.phonepe.questions.bookmyshow.repository.ShowRepository;

import java.time.LocalDateTime;
import java.util.List;
 //https://claude.ai/code/artifact/e675ffac-9443-4147-9f6d-c0db6ad5b6ea?via=auto_preview
/** Admin-facing show scheduling plus the show search users query when browsing. */
public class ShowService {

    private final ShowRepository showRepository;
    private final TheatreService theatreService;
    private final PricingStrategy pricingStrategy;

    public ShowService(ShowRepository showRepository, TheatreService theatreService, PricingStrategy pricingStrategy) {
        this.showRepository = showRepository;
        this.theatreService = theatreService;
        this.pricingStrategy = pricingStrategy;
    }

    public Show createShow(String movieId, String theatreId, String screenId, LocalDateTime startTime, LocalDateTime endTime) {
        Screen screen = theatreService.getScreen(theatreId, screenId);
        Show show = new Show(movieId, theatreId, screenId, startTime, endTime);
        screen.getSeats().forEach(seat -> {
            double price = pricingStrategy.getPrice(show, seat.getSeatType());
            show.addShowSeat(new ShowSeat(show.getId(), seat, price));
        });
        showRepository.save(show);
        return show;
    }

    public Show getShow(String showId) {
        return showRepository.findById(showId)
                .orElseThrow(() -> new EntityNotFoundException("Show not found: " + showId));
    }

    public List<Show> searchShows(String movieId, String city) {
        return showRepository.findByMovieAndCity(movieId, city);
    }

    public void cancelShow(String showId) {
        showRepository.deleteById(showId);
    }
}
service/TheatreService.java 50 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow.service;

import com.practice.phonepe.questions.bookmyshow.entities.Screen;
import com.practice.phonepe.questions.bookmyshow.entities.Seat;
import com.practice.phonepe.questions.bookmyshow.entities.SeatType;
import com.practice.phonepe.questions.bookmyshow.entities.Theatre;
import com.practice.phonepe.questions.bookmyshow.exception.EntityNotFoundException;
import com.practice.phonepe.questions.bookmyshow.repository.TheatreRepository;

import java.util.List;
import java.util.Map;

/** Admin-facing CRUD for theatres, screens and their seating arrangements. */
public class TheatreService {

    private final TheatreRepository theatreRepository;

    public TheatreService(TheatreRepository theatreRepository) {
        this.theatreRepository = theatreRepository;
    }

    public Theatre addTheatre(String name, String city) {
        Theatre theatre = new Theatre(name, city);
        theatreRepository.save(theatre);
        return theatre;
    }

    /**
     * Adds a screen to a theatre and lays out its seats.
     *
     * @param seatsPerRowByType e.g. {"A": NORMAL, "B": PREMIUM} with rowSize seats generated per row
     */
    public Screen addScreen(String theatreId, String screenName, Map<String, SeatType> seatsPerRowByType, int rowSize) {
        Theatre theatre = getTheatre(theatreId);
        Screen screen = new Screen(screenName);
        seatsPerRowByType.forEach((row, seatType) -> {
            for (int seatNumber = 1; seatNumber <= rowSize; seatNumber++) {
                screen.addSeat(new Seat(row, seatNumber, seatType));
            }
        });
        theatre.addScreen(screen);
        return screen;
    }

    public Theatre getTheatre(String theatreId) {
        return theatreRepository.findById(theatreId)
                .orElseThrow(() -> new EntityNotFoundException("Theatre not found: " + theatreId));
    }

    public List<Theatre> findTheatresInCity(String city) {
        return theatreRepository.findByCity(city);
    }

    public Screen getScreen(String theatreId, String screenId) {
        return getTheatre(theatreId).getScreens().stream()
                .filter(screen -> screen.getId().equals(screenId))
                .findFirst()
                .orElseThrow(() -> new EntityNotFoundException("Screen not found: " + screenId));
    }
}
BookMyShowDemo.java 99 lines GitHub ↗
package com.practice.phonepe.questions.bookmyshow;

import com.practice.phonepe.questions.bookmyshow.entities.Booking;
import com.practice.phonepe.questions.bookmyshow.entities.Movie;
import com.practice.phonepe.questions.bookmyshow.entities.Screen;
import com.practice.phonepe.questions.bookmyshow.entities.Seat;
import com.practice.phonepe.questions.bookmyshow.entities.SeatType;
import com.practice.phonepe.questions.bookmyshow.entities.Show;
import com.practice.phonepe.questions.bookmyshow.entities.Theatre;
import com.practice.phonepe.questions.bookmyshow.exception.SeatUnavailableException;
import com.practice.phonepe.questions.bookmyshow.lock.SeatLockManager;
import com.practice.phonepe.questions.bookmyshow.payment.PaymentMethod;
import com.practice.phonepe.questions.bookmyshow.payment.PaymentStrategyFactory;
import com.practice.phonepe.questions.bookmyshow.pricing.DefaultPricingStrategy;
import com.practice.phonepe.questions.bookmyshow.repository.BookingRepository;
import com.practice.phonepe.questions.bookmyshow.repository.InMemoryBookingRepository;
import com.practice.phonepe.questions.bookmyshow.repository.InMemoryMovieRepository;
import com.practice.phonepe.questions.bookmyshow.repository.InMemoryShowRepository;
import com.practice.phonepe.questions.bookmyshow.repository.InMemoryTheatreRepository;
import com.practice.phonepe.questions.bookmyshow.repository.MovieRepository;
import com.practice.phonepe.questions.bookmyshow.repository.ShowRepository;
import com.practice.phonepe.questions.bookmyshow.repository.TheatreRepository;
import com.practice.phonepe.questions.bookmyshow.service.BookingService;
import com.practice.phonepe.questions.bookmyshow.service.MovieService;
import com.practice.phonepe.questions.bookmyshow.service.ShowService;
import com.practice.phonepe.questions.bookmyshow.service.TheatreService;

import java.time.LocalDateTime;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class BookMyShowDemo {

    public static void main(String[] args) throws InterruptedException {
        // --- wiring ---
        MovieRepository movieRepository = new InMemoryMovieRepository();
        TheatreRepository theatreRepository = new InMemoryTheatreRepository();
        ShowRepository showRepository = new InMemoryShowRepository(theatreRepository);
        BookingRepository bookingRepository = new InMemoryBookingRepository();

        MovieService movieService = new MovieService(movieRepository);
        TheatreService theatreService = new TheatreService(theatreRepository);
        ShowService showService = new ShowService(showRepository, theatreService, new DefaultPricingStrategy());
        SeatLockManager seatLockManager = new SeatLockManager(TimeUnit.SECONDS.toMillis(30));
        PaymentStrategyFactory paymentStrategyFactory = new PaymentStrategyFactory();
        BookingService bookingService = new BookingService(bookingRepository, showService, seatLockManager, paymentStrategyFactory);

        // --- admin sets up catalog, theatre and a show ---
        Movie movie = movieService.addMovie("Inception", "English", "Sci-Fi", 148);
        Theatre theatre = theatreService.addTheatre("PVR Forum", "Bengaluru");

        Map<String, SeatType> seatLayout = new LinkedHashMap<>();
        seatLayout.put("A", SeatType.NORMAL);
        seatLayout.put("B", SeatType.PREMIUM);
        seatLayout.put("C", SeatType.RECLINER);
        Screen screen = theatreService.addScreen(theatre.getId(), "Screen 1", seatLayout, 10);

        Show show = showService.createShow(movie.getId(), theatre.getId(), screen.getId(),
                LocalDateTime.now().plusHours(3), LocalDateTime.now().plusHours(5));

        System.out.println("Show created: " + movie + " at " + theatre.getName() + " (" + show.getId() + ")");

        // --- a user browses shows and books a couple of seats end-to-end ---
        List<Show> shows = showService.searchShows(movie.getId(), "Bengaluru");
        System.out.println("Shows found in Bengaluru: " + shows.size());

        Seat targetSeat = screen.getSeats().get(0);
        Booking booking = bookingService.initiateBooking("user-1", show.getId(), Collections.singletonList(targetSeat.getId()));
        System.out.printf("Booking %s created, amount due: %.2f%n", booking.getId(), booking.getTotalAmount());

        Booking confirmed = bookingService.confirmBooking(booking.getId(), PaymentMethod.CARD);
        System.out.println("Booking status after payment: " + confirmed.getStatus());

        // --- concurrency demo: many users race for the same seat ---
        Seat contestedSeat = screen.getSeats().get(1);
        int contenders = 10;
        ExecutorService executor = Executors.newFixedThreadPool(contenders);
        CountDownLatch startGate = new CountDownLatch(1);
        CountDownLatch doneGate = new CountDownLatch(contenders);
        AtomicInteger successCount = new AtomicInteger();
        AtomicInteger rejectedCount = new AtomicInteger();

        for (int i = 0; i < contenders; i++) {
            String userId = "contender-" + i;
            executor.submit(() -> {
                try {
                    startGate.await();
                    bookingService.initiateBooking(userId, show.getId(), Collections.singletonList(contestedSeat.getId()));
                    successCount.incrementAndGet();
                } catch (SeatUnavailableException e) {
                    rejectedCount.incrementAndGet();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    doneGate.countDown();
                }
            });
        }

        startGate.countDown();
        doneGate.await();
        executor.shutdown();

        System.out.printf("Seat %s contested by %d users -> %d locked it, %d were rejected%n",
                contestedSeat.getLabel(), contenders, successCount.get(), rejectedCount.get());

        seatLockManager.shutdown();
    }
}