Flight/Seat/Booking/FareClass classes reusing Movie Ticket Booking's seat-map and hold-expiry patterns, plus the class-design extension needed for multi-leg itineraries.
Published September 23, 2026
class Flight { String flightNumber; Airport origin, destination; Instant departure; List<Seat> seats; }
class FareClass { String name; double priceMultiplier; } // Economy, Premium, Business, First
class Seat { String seatNumber; FareClass fareClass; }
class Passenger { String name; String passportNumber; }
class Booking { List<Flight> legs; Map<Flight, Seat> seatAssignments; Passenger passenger; BookingStatus status; }
Seat selection and the double-booking risk here are structurally identical to Movie Ticket Booking System — seats are per-flight (not a permanent property of Seat), and selection needs the same temporary-hold-with-expiry pattern (a passenger selecting seat 14C shouldn't permanently lock it away if they abandon checkout). Recognizing this as the same underlying problem, not a new one, is worth stating explicitly in an interview — reapplying an already-designed solution is a stronger signal than re-deriving seat-locking from scratch a second time.
class Booking {
List<Flight> legs; // e.g. [JFK->LHR, LHR->CDG] for a connecting itinerary
Map<Flight, Seat> seatAssignments; // one seat assignment PER LEG — a passenger picks a different seat on each flight
}
The key design change: Booking holds a list of flights (legs), not a single Flight, and seat assignment becomes a map keyed by leg, since a passenger's seat on the first flight of a connection has nothing to do with their seat on the second. The seat-hold-and-confirm flow from a single-leg booking applies per leg independently — holding seat 14C on leg 1 and seat 22A on leg 2 are two separate hold operations, and a booking should only fully confirm once every leg's hold succeeds (an all-or-nothing multi-leg commit, conceptually similar to the atomicity concern Two-Phase Commit discusses at the distributed-systems level, just within a single booking transaction here).
The per-flight seat inventory is where concurrency bugs live, so it deserves real code. Keep the state of each seat on that flight in a FlightSeatMap, not on Seat, because the same physical seat is sold separately on every departure:
enum SeatState { AVAILABLE, HELD, BOOKED }
class SeatHold {
final String holdId; final String seatNumber; final String bookingId; final Instant expiresAt;
SeatHold(String holdId, String seatNumber, String bookingId, Instant expiresAt) {
this.holdId = holdId; this.seatNumber = seatNumber; this.bookingId = bookingId; this.expiresAt = expiresAt;
}
}
class FlightSeatMap {
private final Map<String, SeatState> states = new HashMap<>();
private final Map<String, SeatHold> holds = new HashMap<>();
private final Clock clock;
private final Duration holdDuration = Duration.ofMinutes(10);
FlightSeatMap(Collection<Seat> seats, Clock clock) {
this.clock = clock;
seats.forEach(s -> states.put(s.seatNumber, SeatState.AVAILABLE));
}
synchronized Optional<SeatHold> hold(String seatNumber, String bookingId) {
releaseExpired();
if (states.get(seatNumber) != SeatState.AVAILABLE) return Optional.empty();
SeatHold hold = new SeatHold(UUID.randomUUID().toString(), seatNumber, bookingId,
clock.instant().plus(holdDuration));
states.put(seatNumber, SeatState.HELD);
holds.put(seatNumber, hold);
return Optional.of(hold);
}
synchronized boolean confirm(SeatHold hold) {
releaseExpired();
SeatHold current = holds.get(hold.seatNumber);
if (current == null || !current.holdId.equals(hold.holdId)) return false; // expired or taken over
states.put(hold.seatNumber, SeatState.BOOKED);
holds.remove(hold.seatNumber);
return true;
}
synchronized void release(SeatHold hold) {
SeatHold current = holds.get(hold.seatNumber);
if (current != null && current.holdId.equals(hold.holdId)) {
holds.remove(hold.seatNumber);
states.put(hold.seatNumber, SeatState.AVAILABLE);
}
}
private void releaseExpired() {
Instant now = clock.instant();
holds.values().removeIf(h -> {
boolean expired = !h.expiresAt.isAfter(now);
if (expired) states.put(h.seatNumber, SeatState.AVAILABLE);
return expired;
});
}
}
Design points to call out:
confirm compares hold IDs, not just seat numbers. If a hold expired and someone else re-held the seat, the first customer's late confirmation fails instead of stealing the seat.Clock is injected, so tests can advance time and verify expiry without sleeping.synchronized becomes a database row update with a condition (UPDATE seat SET state='HELD' … WHERE state='AVAILABLE'), or a Redis SET NX PX. The same rules apply.class BookingService {
Booking bookItinerary(Passenger p, Map<Flight, String> requestedSeats) {
List<SeatHold> acquired = new ArrayList<>();
String bookingId = UUID.randomUUID().toString();
try {
for (var e : requestedSeats.entrySet()) {
SeatHold hold = seatMapFor(e.getKey()).hold(e.getValue(), bookingId)
.orElseThrow(() -> new SeatUnavailableException(e.getKey(), e.getValue()));
acquired.add(hold);
}
return new Booking(bookingId, p, requestedSeats, BookingStatus.PENDING_PAYMENT, acquired);
} catch (SeatUnavailableException ex) {
acquired.forEach(h -> seatMapFor(flightOf(h)).release(h)); // compensate the legs already held
throw ex;
}
}
}
Holding each leg in turn and releasing everything on the first failure is a small, local saga: acquire step by step, and compensate in reverse on failure. Payment then confirms every hold, and if any confirmation fails (a hold expired during a slow payment), the booking is refunded or re-attempted rather than left half-confirmed.
To avoid deadlock-like contention when two customers request the same pair of seats in opposite leg order, acquire holds in a consistent order (by flight ID, for example).
Q: What happens if seat hold succeeds for leg 1 but fails for leg 2 (e.g. the flight fills up between requests)? A: The booking needs to roll back leg 1's hold too — a multi-leg booking should be all-or-nothing, not leaving a passenger with a confirmed seat on one leg of a trip they can't complete, which is exactly why the hold-then-confirm-all-or-release-all pattern matters more here than in the single-leg Movie Ticket case.
Q: How would FareClass pricing interact with dynamic, demand-based airline pricing? A: Similar to Hotel Reservation System's pricing discussion — priceMultiplier as a static field is a simplification; real airline pricing is highly dynamic (demand, days-to-departure, historical booking curves), which argues for a pluggable PricingStrategy rather than a fixed field, following the same Strategy pattern reasoning used throughout this course.
Q: Should seat assignment happen at booking time or be deferrable to check-in? A: Both are legitimate, different product decisions — some airlines assign seats at booking (matching this design directly), others allow booking without a specific seat and assign one at check-in (a different, simpler data model where Booking doesn't need seatAssignments until later) — worth naming as an explicit scope-clarifying question at the start of the exercise, per the Object-Oriented Design Refresher's scoping guidance.
Q: Why store seat state per flight instead of a booked flag on Seat?
A: A Seat is part of the aircraft's layout and is reused on every departure. Availability is a fact about one seat on one flight. Putting it on Seat would make row 14C "booked" on every future flight of that aircraft.
Q: How do you test hold expiry without waiting ten minutes?
A: Inject a Clock (or a time-supplier interface) into the seat map. Tests use a mutable fake clock, place a hold, advance the clock past the hold duration, and assert that the seat is available again and that the old hold can't be confirmed.
Q: How would overbooking fit into this model? A: Airlines deliberately sell more tickets than seats in a fare bucket, expecting no-shows. That's inventory at the fare-class count level (sell up to capacity × 1.05), separate from seat assignment, which remains strict. Seat maps then allow some bookings to have no seat until check-in.