Code · LLD machine coding · PhonePe prep
BookMyShow
Design the backend for booking movie tickets.
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);
}
} 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();
}
}