CMD Guide
HomeOO & Low-Level DesignOO Design Problems

Design an Airline Management System

Design an Airline Management System

An airline management system controls the operations of an airline: scheduling flights, reserving tickets, cancelling flights, handling payments, assigning crew, and notifying customers. The hard part of the design is not the entity list — it is the one moment where two customers reach for the same physical seat on the same physical departure at the same instant. Everything below builds toward getting that single moment right.

Requirements

  1. Customers search flights by date and source/destination airport.
  2. Customers reserve tickets for a scheduled flight and can build a multi-flight itinerary.
  3. Users can view schedules, departure/arrival times, available seats, and flight details.
  4. A reservation can cover multiple passengers under one itinerary.
  5. Only an admin adds aircraft, flights, and schedules, and can cancel a scheduled flight (all stakeholders are notified).
  6. Customers can cancel a reservation or a whole itinerary.
  7. The system assigns pilots and crew to flights.
  8. The system handles payments.
  9. The system sends notifications on reservation changes and flight-status updates.

Actors and use cases

Actors: Admin (manages flights, schedules, staff), Front-desk officer (reserves/cancels on a customer's behalf), Customer (searches, reserves, cancels), Pilot/Crew (view assignments), System (sends notifications). Top use cases: search flights, create/modify/view reservation, assign seats, make payment, update flight schedule, assign pilots and crew.

The core modelling decision: Flight vs FlightInstance vs FlightSeat

The single most important split in this design is the difference between a flight and a flight instance, and what a seat means at each level.

Why this matters for correctness: two customers competing for "14C" are only in conflict if they are competing for the same FlightInstance's FlightSeat. That shared object is the natural place to put the lock. A per-reservation object is the wrong place — each customer holds their own reservation, so locking it guards nothing shared (we return to this in step 7).

diagram
diagram

Walking the reservation, step by step

Reserving a seat is a seven-step path, and only the last step is subtle.

  1. Search. The customer queries flights by date and route. The system resolves matching Flight objects and, through their schedules, the FlightInstances on the requested date.
  2. Pick an instance. The customer chooses one departure — the specific FlightInstance (BA212 on 29 Jun).
  3. Materialise seats. The instance must expose a concrete seat map: one FlightSeat per aircraft Seat, each with a fare and a free/reserved flag. This is real work — the split between Seat and FlightSeat only pays off if the instance actually builds those per-departure seats.
  4. Choose passengers and seats. The customer names passengers and requests seats (or asks the system to pick any free one).
  5. Build the reservation. A FlightReservation is created against the chosen instance, holding the passenger→seat map.
  6. Atomically claim each seat. Before the reservation can be confirmed, the instance must atomically move 14C from free to reserved for exactly one winner. This is the load-bearing line.
  7. Pay and confirm. On a successful claim the customer pays; the reservation moves to CONFIRMED and a notification is sent. If the claim loses the race, the customer is told the seat is gone and offered another.

Steps 3 and 6 are where most write-ups quietly hand-wave. The promise of the Seat/FlightSeat split is only real if expansion (step 3) and the atomic claim (step 6) actually do something — so we show both in full below, with every member they rely on declared.

Code — supporting types

For brevity, getters/setters are omitted unless they carry meaning; assume fields are private with conventional accessors. Enums (FlightStatus, ReservationStatus, SeatClass, SeatType, etc.) are as in the standard model. We show the seat and people types first, then the flight/reservation types, then the concurrency-critical method.

Seat and FlightSeat. The split is only meaningful if a FlightSeat can be built from an aircraft Seat and can carry per-departure state. So we add the constructors and the reserved-state members that the later code depends on — they are declared here, not assumed.

public class Seat {
    private final String seatNumber;
    private final SeatType type;
    private final SeatClass _class;

    // Declared so FlightSeat can copy a Seat's identity via super(...).
    public Seat(String seatNumber, SeatType type, SeatClass _class) {
        this.seatNumber = seatNumber;
        this.type = type;
        this._class = _class;
    }
    public String getSeatNumber() { return seatNumber; }
    public SeatType getType()     { return type; }
    public SeatClass getSeatClass() { return _class; }
}

public class FlightSeat extends Seat {
    private final double fare;
    // Per-departure state. Guarded by the owning FlightInstance's monitor;
    // never mutated except while that monitor is held (see reserveSeat).
    private boolean reserved = false;

    // Copies the aircraft Seat's identity, then adds per-instance fare.
    public FlightSeat(Seat seat, double fare) {
        super(seat.getSeatNumber(), seat.getType(), seat.getSeatClass());
        this.fare = fare;
    }
    public double getFare()    { return fare; }
    public boolean isReserved() { return reserved; }
    // Package-private: only FlightInstance, while holding its lock, flips this.
    void markReserved()        { this.reserved = true; }
    void markFree()            { this.reserved = false; }
}

People. Unchanged from the standard model.

public abstract class Person {
    private String name; private Address address;
    private String email; private String phone;
    private Account account;
}
public class Customer extends Person {
    private String frequentFlyerNumber;
    public List<Itinerary> getItineraries();
}
public class Passenger {
    private String name; private String passportNumber; private Date dateOfBirth;
    public String getPassportNumber() { return this.passportNumber; }
}

Code — Flight, FlightInstance, FlightReservation

The lock lives on FlightInstance. The instance is the one object shared by every customer competing for 29-Jun's seats, so it owns the materialised seatMap and is the monitor that serialises claims. FlightReservation does not mutate seat state itself — it asks the shared instance to do the atomic claim.

public class Flight {
    private String flightNumber;
    private Airport departure; private Airport arrival;
    private int durationInMinutes;
    private List<WeeklySchedule> weeklySchedules;
    private List<CustomSchedule> customSchedules;
    private List<FlightInstance> flightInstances;
}

public class FlightInstance {
    private Date departureTime;
    private String gate;
    private FlightStatus status;
    private Aircraft aircraft;

    // The authoritative, per-departure seat map. ALL access is guarded
    // by 'this' (the FlightInstance monitor). seatNumber -> FlightSeat.
    private final Map<String, FlightSeat> seatMap = new HashMap<>();

    // Step 3: materialise one FlightSeat per aircraft Seat for THIS departure.
    // Idempotent; call once when the instance is created/scheduled.
    public synchronized void materializeSeats() {
        if (!seatMap.isEmpty()) return;
        for (Seat s : aircraft.getSeats()) {
            seatMap.put(s.getSeatNumber(), new FlightSeat(s, defaultFareFor(s)));
        }
    }

    private double defaultFareFor(Seat s) {
        // Fare policy by class/type; stubbed for the model.
        switch (s.getSeatClass()) {
            case FIRST_CLASS: return 2000.0;
            case BUSINESS:    return 1200.0;
            default:          return 300.0;
        }
    }

    // Step 6 - THE load-bearing method. Locks the SHARED instance, so every
    // customer racing for 14C on this departure contends on the same monitor.
    // Returns the claimed seat, or null if it was already taken (caller retries
    // or offers another seat). seatNumber == null means "any free seat".
    public synchronized FlightSeat reserveSeat(String seatNumber) {
        FlightSeat seat = (seatNumber == null)
                ? findFreeSeat()
                : seatMap.get(seatNumber);
        if (seat == null || seat.isReserved()) {
            return null;            // unknown seat, or lost the race
        }
        seat.markReserved();        // free -> reserved, atomically under this lock
        return seat;
    }

    // Releases a seat on cancellation. Same monitor as the claim.
    public synchronized void releaseSeat(String seatNumber) {
        FlightSeat seat = seatMap.get(seatNumber);
        if (seat != null) seat.markFree();
    }

    // Caller already holds 'this' (invoked only from synchronized methods).
    private FlightSeat findFreeSeat() {
        for (FlightSeat fs : seatMap.values()) {
            if (!fs.isReserved()) return fs;
        }
        return null;
    }

    public boolean cancel() { /* set CANCELLED, notify */ return true; }
    public void updateStatus(FlightStatus status) { this.status = status; }
}

FlightReservation delegates the claim. It holds the passenger→seat map for its own booking, but it never flips seat state directly — it routes every claim through the shared FlightInstance, which is the only object that can serialise competing customers.

public class FlightReservation {
    private String reservationNumber;
    private final FlightInstance flight;        // the SHARED departure
    private final Map<Passenger, FlightSeat> seatMap = new HashMap<>();
    private Date creationDate;
    private ReservationStatus status = ReservationStatus.REQUESTED;

    public FlightReservation(String reservationNumber, FlightInstance flight) {
        this.reservationNumber = reservationNumber;
        this.flight = flight;
    }

    // Step 5+6: try to claim 'seatNumber' (or any free seat) for p.
    // NOTE: NOT synchronized on 'this'. Mutual exclusion comes from
    // flight.reserveSeat(...), which locks the shared FlightInstance.
    // Two customers hold two different FlightReservation objects, so a
    // lock on 'this' would guard nothing they share -- it would NOT stop
    // a double-sell. The shared instance is the correct monitor.
    public boolean reserveSeat(Passenger p, String seatNumber) {
        FlightSeat claimed = flight.reserveSeat(seatNumber);  // atomic on the instance
        if (claimed == null) {
            return false;        // seat gone; caller offers another / retries
        }
        seatMap.put(p, claimed);
        this.status = ReservationStatus.PENDING;  // -> CONFIRMED after payment
        return true;
    }

    public static FlightReservation fetchReservationDetails(String reservationNumber);
    public List<Passenger> getPassengers() { return new ArrayList<>(seatMap.keySet()); }
}

Why the lock target is the whole point

It is tempting to write public synchronized boolean reserveSeat(...) on FlightReservation and call it safe. It is not, and this is the classic trap.

synchronized on an instance method locks this — the receiver object. Two customers racing for 14C on the 29-Jun departure are each acting through their own FlightReservation. Those are two distinct objects, hence two distinct monitors. A lock on reservation A and a lock on reservation B exclude nobody from each other: both threads enter their own synchronized method simultaneously, both read 14C as free, both write it as theirs. The seat is double-sold, and the keyword did nothing — it guarded a private object nobody else was contending for.

The fix is structural, not cosmetic: lock the object that is actually shared by the competitors. That is the FlightInstance, which owns the single authoritative seatMap. FlightInstance.reserveSeat is synchronized (locking the shared instance), and the check-then-set — isReserved() then markReserved() — happens entirely inside that one monitor, so it is atomic with respect to every other claim on the same departure. The reservation simply delegates. Exactly one thread observes 14C free and flips it; the other observes it reserved and gets null.

Rule of thumb: a lock only excludes threads that contend on the same monitor object. Put the lock on the shared, contended resource — here the per-departure seat map — never on the per-request object each thread already owns privately. (For finer granularity you could lock per-FlightSeat or use a ConcurrentHashMap with an atomic compute; locking the instance is the simplest correct baseline.)

Cancellation, payment, itinerary

Cancelling a reservation calls flight.releaseSeat(seatNumber) (same monitor as the claim), sets the reservation to CANCELLED, and notifies stakeholders. Itinerary aggregates several FlightReservations under one customer; makeReservation() drives the per-leg claims and makePayment() settles them, flipping confirmed legs to CONFIRMED. A failed claim on any leg lets the itinerary roll back the legs already held by calling releaseSeat on each.

The honest limit: what the monitor buys you, and what it does not

synchronized on the FlightInstance is the correct baseline — within a single JVM. That qualifier is the whole scale story, and a senior answer states it rather than letting the interviewer find it. A real airline runs the reservation service on many app servers behind a load balancer, and the seat map is not a HashMap in one heap — it is rows in a shared database. An in-process monitor on server 1 excludes nobody on server 2: two customers routed to two different servers each enter their own instance's synchronized reserveSeat, each reads 14C as free in its own copy, and the seat is double-sold again — the same failure the monitor fixed within one process, resurrected across processes.

Where the lock really goes at scale (the named alternative). The serialization must move to the one thing all the servers share — the database row for that FlightSeat. Two equivalent techniques:

Read the synchronized version on this page as the single-node teaching model of that database CAS — same shape, same check-then-set, different monitor. The in-JVM lock is not "wrong"; it is honest only for one process, and naming that boundary is the point.

The operability gap between claim and payment

Step 6 claims the seat; step 7 takes payment. Between them 14C is reserved but unpaid — and payment can fail, or the customer can simply close the tab. A naive claim strands the seat reserved forever, so the flight departs with an empty, unsellable 14C. Production designs make the claim a time-boxed hold: set reserved_until = now + N minutes at claim time, confirm only if the hold has not expired, and run a sweeper that returns expired holds to free (calling the same releaseSeat path). A process crash after markReserved but before the reservation is persisted leaks a seat the same way — reconcile by treating the persisted reservation as the source of truth: any seat marked reserved with no owning PENDING/CONFIRMED reservation past its hold TTL is swept free. Make the claim idempotent — a retry carrying the same reservationNumber re-claiming its own held seat returns success, not a second booking.

Interview drills

Q1. You put synchronized on FlightInstance and it passed your local test. It now runs on 20 app servers. Is 14C still safe?
No. The monitor only excludes threads inside one JVM; two servers have two separate FlightInstance objects (or two separate reads of the same row), so both can claim 14C. Move the check-and-set to the shared store — a conditional UPDATE ... WHERE reserved = false whose rows-affected decides the winner, or SELECT ... FOR UPDATE inside the booking transaction.

Q2. A customer claims 14C, then abandons checkout. What happens to the seat, and how do you prevent a permanent leak?
Without a hold policy it stays reserved forever and the flight loses a sellable seat. Make the claim a TTL hold (reserved_until), confirm only if the hold is still valid, and run a sweeper that frees expired holds. Same mechanism covers a crash between claim and persistence — the persisted reservation is the source of truth; unowned reserved seats past TTL are swept free.

Q3. Why is locking FlightReservation wrong, in one sentence?
Because each customer holds their own FlightReservation, so its monitor guards a private object nobody else contends for; the lock must sit on the object the competitors share — the FlightInstance's seat map (or its DB row).

Q4. Airlines deliberately oversell. Does that break this model?
No — it lives one level up. The FlightInstance seat map stays authoritative for physical assignment (14C is sold once), while an overbooking policy governs how many tickets may be issued against the instance before assignment. Keep the two concerns separate: selling a ticket is not the same operation as claiming a seat, and only the latter needs the per-seat CAS.

Source

Adapted and corrected from the standard "Design an Airline Management System" object-oriented design problem (Grokking the Object-Oriented Design Interview / educative.io), part of this guide's OO & Low-Level Design → OO Design Problems track. The class structure (Airline, Airport, Aircraft, Flight, FlightInstance, FlightReservation, FlightSeat, Itinerary, Payment, Notification) follows that canonical model; the seat-materialisation method, the seat-claim concurrency control, and the supporting Seat/FlightSeat constructors and members shown here are this guide's corrections to make the reservation path compile and to place the lock on the shared FlightInstance rather than on the per-customer reservation.

🤖 Don't fully get this? Learn it with Claude

Stuck on Design an Airline Management System? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Design an Airline Management System** (OO & Low-Level Design) and want to truly understand it. Explain Design an Airline Management System from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Design an Airline Management System** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Design an Airline Management System** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Design an Airline Management System** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes