Designing an Airline Booking System

September 25, 202615 min readUpdated 8/22/2026

Airline booking is the hardest inventory problem in this track, and it is worth doing last because it breaks the answers that worked in the previous two.

A hotel night can be resold tomorrow. A seat on a flight that has departed is worth exactly zero, forever — which is why airlines deliberately sell more seats than they have, price the same seat at wildly different amounts, and run pricing engines that change those amounts several times a day. The engineering follows from that economics, not the other way round.

As with the Amazon post, this is design work rather than a running application: nobody operates a global distribution system on a laptop. Where a mechanism has already been shown working earlier in the track, it is linked rather than re-invented.

Step 1 — Scope

IN SCOPE                           OUT OF SCOPE (say so)
  search flights                     loyalty programmes
  fares and seat availability        crew and aircraft scheduling
  hold a seat during payment         cargo
  book, pay, ticket                  check-in and boarding
  seat selection                     the actual pricing science
  cancel / change

WORTH ASKING
  one airline or an aggregator?    -> decides whether you OWN the inventory
  connecting flights?              -> makes search a graph problem, not a lookup
  overbooking allowed?             -> yes, and it is a business requirement
  how long is a seat held?         -> minutes, and it is contractual

NON-FUNCTIONAL
  search: fast, may be stale        prices are quoted, then re-validated
  booking: correct, never stale     selling seat 14A twice is a real incident
  read:write ~ 1000:1               enormous browsing, few bookings
  a ticket must NEVER be lost       it is a financial instrument

The read-to-write ratio is the largest in this track. People search flights obsessively and book rarely, which means the search path and the booking path are effectively two different systems with different requirements.

Step 2 — Estimate

ASSUME  a large carrier: ~5,000 flights/day · ~150 seats each
        ~1M searches/day per 10k bookings

SEATS     5,000 x 150            = 750k seats/day
          x 330 days of schedule = ~250M seat-inventory records live at once

SEARCHES  1M / 100k              = 10 searches/sec   peak x5 = 50/sec
          BUT each search fans out:
            "LHR -> LAX, flexible +/- 3 days, 1 stop allowed"
            = hundreds of candidate itineraries priced per search

BOOKINGS  10k/day / 100k         = 0.1/sec          peak x10 = 1/sec

One booking per second at peak. Once again — as with Airbnb and Amazon — the write volume is trivial and the write path is where all the difficulty is.

The interesting number is the fan-out on the second block. A single user search is not one lookup; it is a search over combinations of flights, dates and fare classes, each of which must be priced. The work per search is hundreds of times the work per request, which is why flight search is famously expensive and why every airline caches aggressively.

Step 3 — Search is a graph problem

“London to Los Angeles” is not a row lookup. Direct flights are one answer; one-stop itineraries are a join of two flights whose times connect; two-stop is another level.

   LHR ──────────── direct ──────────────> LAX

   LHR ──> JFK ──> LAX        valid if:  arrival + minimum connect time
   LHR ──> AMS ──> LAX                     <= departure of the next leg
   LHR ──> DUB ──> ORD ──> LAX           and total duration is reasonable
                                          and the airports are the same terminal-ish

   Airports x flights x dates x fare classes = a combinatorial explosion.

The mechanism that makes it tractable is the same one as everywhere else in this track: precompute the expensive part offline and look it up online.

Route structures — which sequences of flights physically connect, given minimum connection times — change only when the schedule changes, which is weekly. So they are computed in batch and stored. A search then becomes: look up candidate itineraries for this origin-destination pair, then price and filter them, rather than exploring a graph at request time.

Pricing the survivors is still expensive, which is why results are cached per origin-destination-date and quoted as indicative. The price is re-validated before payment — and the fact that a quoted fare can vanish is a genuine product behaviour rather than a bug, which is unusual and worth saying out loud.

Step 4 — Inventory is not a seat count

This is the part that surprises people. An airline does not sell “150 seats”. It sells buckets, and the buckets are the pricing system.

   Flight DL123, 2026-11-04, 150 physical seats

   fare class    seats     price     rules
   ──────────────────────────────────────────────────────────────
   J  business      12    $4,200     changeable, refundable
   Y  full econ     10      $980     changeable
   M  economy       25      $520     change fee
   K  economy       40      $380     non-refundable
   T  economy       48      $240     non-refundable, no bags
   ──────────────────────────────────────────────────────────────
                   135 sold as fare classes, 15 held back

   Same physical cabin. Five different products.

Revenue management moves seats between buckets continuously as departure approaches — closing the cheap classes as the flight fills, reopening them if it does not. So “is there a seat?” is not a question about capacity, it is a question about whether a bucket has availability at a price.

CREATE TABLE flight_inventory (
    flight_id    BIGINT      NOT NULL,
    departure    DATE        NOT NULL,
    fare_class   CHAR(1)     NOT NULL,   -- J, Y, M, K, T ...
    authorised   INTEGER     NOT NULL,   -- how many MAY be sold in this bucket
    sold         INTEGER     NOT NULL,
    held         INTEGER     NOT NULL,   -- in a checkout right now
    PRIMARY KEY (flight_id, departure, fare_class)
);
-- available = authorised - sold - held

Note that authorised can exceed the physical seat count, across buckets and in total. That is not a bug; it is overbooking, and it is the next section.

The primary key is worth a moment: the contended row is now one fare class on one flight-date rather than the flight as a whole, which distributes contention across five rows instead of one. That is the sharded-counter trick from the Amazon post, arriving naturally out of the domain model rather than being bolted on.

Step 5 — Overbooking is a requirement

Roughly 5–15% of passengers do not show up. A flight that departs with empty seats sold at zero is pure loss, and the seat cannot be sold later — the inventory is perishable in the strictest sense.

   150 physical seats
   historical no-show rate for this route/day/season: 8%
   -> authorise ~162 seats

   most departures:  ~149 people board. Full flight, nothing wasted.
   occasionally:     152 people board. 2 must be denied.
                     -> voluntary compensation first, then involuntary
                     -> the cost of that is BUDGETED, not an incident

Engineering-wise this changes one thing profoundly: the system is designed to sell more than it has, so “prevent overselling” is not the goal. The goal is to sell exactly the authorised number, where the authorised number is a forecast.

That reframes the correctness requirement. Selling 163 when 162 were authorised is a bug — the count must be exact against the authorisation. Selling 162 against 150 physical seats is the system working as designed. Being able to state that distinction clearly is most of what this question is testing.

Step 6 — The seat hold

The concurrency problem, and it is the Airbnb hold at much worse contention: on a popular flight, dozens of people are in checkout for the same fare class simultaneously.

-- Take the hold atomically. The guard is IN the statement.
UPDATE flight_inventory
   SET held = held + 1
 WHERE flight_id = 123 AND departure = '2026-11-04' AND fare_class = 'K'
   AND authorised - sold - held >= 1;
-- 0 rows updated? That bucket is full. Offer the next fare class up.

Then the hold has to expire, because people abandon checkout constantly.

CREATE TABLE seat_holds (
    hold_id     UUID        PRIMARY KEY,
    flight_id   BIGINT      NOT NULL,
    departure   DATE        NOT NULL,
    fare_class  CHAR(1)     NOT NULL,
    expires_at  TIMESTAMPTZ NOT NULL,     -- typically 10-20 minutes
    session_id  UUID        NOT NULL
);
CREATE INDEX ix_holds_expiry ON seat_holds (expires_at);

Two things about expiry are worth getting right.

Do not rely on a sweeper alone. A background job releasing expired holds is necessary, and it runs on an interval — so between a hold expiring and the sweep, the seat is invisible. Availability queries should therefore treat expires_at < now() as already released, and let the sweeper do the bookkeeping afterwards. Correct immediately, tidy eventually.

The sweeper must run exactly once. Two instances releasing the same hold double-decrement the counter, and the fix is to make release conditional on the hold row still existing — delete-and-decrement in one transaction, so the delete is the guard. Same turn-a-delta-into-a-fact move as the Amazon post.

Specific seats are a second problem

Choosing 14A is separate from buying a fare-class seat, and it has its own uniqueness rule:

CREATE TABLE seat_assignments (
    flight_id   BIGINT  NOT NULL,
    departure   DATE    NOT NULL,
    seat        CHAR(4) NOT NULL,        -- '14A'
    booking_id  BIGINT  NOT NULL,
    PRIMARY KEY (flight_id, departure, seat)   -- the uniqueness IS the guard
);

The composite primary key makes double-assignment impossible, which is the constraint-not-coordination principle again — and here it is the portable version, since a unique key needs no database-specific feature.

Note that overbooking makes seat assignment genuinely fallible: if 162 tickets exist for 150 seats, twelve of them cannot have an assignment. Airlines handle this by assigning at check-in rather than at booking for some fare classes, which is a product decision the schema has to allow.

How much contention, really

Worth a moment of arithmetic, because “one booking per second” sounds harmless and the contention profile is not.

   average:  1 booking/sec spread over ~5,000 flight-dates
             -> collisions essentially never

   a sale on one popular route:
             500 people in checkout for ONE flight-date
             competing for ~40 seats in fare class K
             -> every single request contends on ONE row

This is the point the concurrency post makes about contention being concentration rather than volume, in its most extreme form in this track. The system-wide write rate is negligible; the per-row write rate during a sale is brutal.

Which is why the fare-class key matters so much. Splitting inventory into five buckets divides contention five ways for free, and if that is not enough, the same trick extends — partition a bucket into sub-counters and sum them for display, at the cost of a bucket appearing empty while another has seats.

Step 7 — Booking, PNR and ticketing

Three distinct things that people conflate, and the distinction is the domain model.

   PNR       the reservation record — who, what flights, contact details
             a 6-character code: "X7K2QP"
             exists as soon as you book, before any money moves

   TICKET    the financial document — proof of payment, per passenger
             a 13-digit number
             issued AFTER payment settles

   COUPON    one flight leg on a ticket
             consumed at boarding; the unit that gets refunded or reissued

A booking without a ticket is a reservation that will be cancelled if not paid. A ticket is a financial instrument with its own lifecycle: it can be exchanged, refunded, reissued or voided, and each of those is an accounting event.

The engineering consequence: ticket issuance must be exactly-once in its effects, because a duplicate ticket is a duplicate charge. That is the idempotency-key pattern from the concurrency post, applied to the step that touches money.

And the whole flow is a saga, because it spans inventory, payment and a ticketing system:

   1 hold seats            compensate: release the hold
   2 create PNR            compensate: cancel the PNR
   3 authorise payment     compensate: void the authorisation
   4 issue ticket          compensate: void the ticket (a real operation)
   5 capture payment       compensate: refund

   Fail at step 4 -> void nothing yet, release the hold, cancel the PNR.
   Fail at step 5 -> the ticket exists and is unpaid: void it, alert a human.

Step 8 — Changes and cancellations

Airbnb’s cancellation rule was one cutoff date. Here it is a matrix, and the matrix is attached to the fare rather than to the booking.

   fare class   change            cancel                 no-show
   ─────────────────────────────────────────────────────────────────
   J business   free              full refund            refundable
   Y full econ  free              full refund            refundable
   M economy    $75 + fare diff   refund minus $200      forfeit
   K economy    $150 + fare diff  credit only, expires   forfeit
   T basic      NOT PERMITTED     no refund              forfeit

Two engineering points fall out of that table.

“Fare difference” means a change is a re-pricing, not an edit. Moving to another flight requires checking availability in the new fare class, computing the difference against today’s prices, collecting or refunding it, and reissuing the ticket — which is another saga touching inventory, payment and ticketing. Modelling a change as an update to a row is the mistake; it is a cancellation and a rebooking, recorded as such.

The rules must live in one place, exactly as in the Airbnb post, because at least three surfaces need them: the UI deciding whether to show a Change button, the API enforcing the rule, and the accounting system computing the refund. Three implementations means three answers.

And there is a genuine domain subtlety worth mentioning: releasing a cancelled seat back to inventory is not simply a decrement. Revenue management may want it back in a different fare class than it was sold from — a seat sold cheap six months ago and cancelled the week before departure should return to an expensive bucket, not a cheap one.

Step 9 — Talking to systems older than the web

An aggregator does not own the inventory. It asks a global distribution system, and those predate almost everything in this track.

  • They are slow and rate-limited. Availability lookups take hundreds of milliseconds and you are billed per query, which means aggressive caching is not an optimisation but a commercial necessity.
  • Their data is authoritative and yours is a copy. Every cached price is provisional and must be re-validated before payment.
  • They fail in ways your code must survive. Timeouts, partial responses, and occasionally a booking that succeeded upstream while the response was lost — which is exactly the two generals problem, so every call needs an idempotency key and a reconciliation path.

The architectural answer is the standard one: an adapter per provider behind one interface, circuit breakers, and per-provider rate limiting so a slow partner cannot consume your workers. “A slow dependency with no timeout occupies every worker” is the failure from the load balancing post, and it is at its most likely here.

Step 10 — When a storm cancels four hundred flights

Irregular operations are the airline-specific problem, and they are what makes the whole system harder than a booking site.

   one storm -> 400 cancellations -> ~60,000 passengers to rebook
                                     each on a network already near capacity

   AND simultaneously:
     - every one of them opens the app at once      (traffic spike)
     - each rebooking consumes scarce inventory     (contention spike)
     - downstream connections cascade               (one delay -> many)

The design implications are worth naming because they are unusual:

Rebooking must be automated and prioritised, because sixty thousand manual rebookings is not a thing that can happen. Priority is a business rule — status, fare class, onward connections — and it belongs in one place, like the cancellation policy in the Airbnb post.

The spike is in contention, not just traffic. Sixty thousand people competing for a few thousand seats is the worst possible concurrency profile, and it is exactly when the inventory counters must not be wrong. Queueing rebookings per flight — one writer per contended row — is more robust here than optimistic retries, which would mostly fail.

Degrade deliberately. Seat selection, upgrades and ancillary sales can all be disabled to protect the rebooking path. Same principle as the Amazon post: rank features by whether they matter, and shed in reverse order.

Failure modes

What failsEffectDegradation
Search cacheEvery search hits pricing and the GDS The most expensive failure here, because those calls are billed and rate-limited. Shed load and serve stale results rather than passing the traffic through.
A GDS partnerThat airline’s flights disappear from results Circuit-break it and show the rest. Partial results beat an error page.
Inventory serviceNo holds, no bookings No safe degradation — unlike retail, you cannot optimistically accept and reconcile, because a seat cannot be manufactured. Refuse cleanly.
PaymentPNRs created, unticketed They expire naturally by the ticketing deadline, releasing the seats. The domain already has this concept, which is convenient.
TicketingPayment captured, no ticket The one that must never be silent. Alert immediately; a human resolves it. Money without a corresponding document is an accounting incident.

The third row is the sharpest contrast with the Amazon post. Retail can accept an order it may not be able to fulfil, because stock can be replenished and the customer can wait. An airline cannot: the seat either exists on that aircraft or it does not, and there is no equivalent of a backorder. So the same architectural question — what do we do when inventory is unavailable? — gets opposite answers in two systems that look superficially alike.

The whole thing

   SEARCH (50/s, huge fan-out)          BOOK (1/s, high contention)

   GET /search                          POST /bookings
       │                                    │
       v                                    │ saga:
   [ cache: O-D-date ]                      ├─> hold seats (conditional UPDATE)
       │ miss                               ├─> create PNR
       v                                    ├─> authorise payment
   [ itinerary builder ]                    ├─> issue ticket (idempotent)
       │  precomputed route graph           └─> capture payment
       v                                        │
   [ pricing / fare rules ]                     v
       │                                  [ inventory ]  keyed by
       └──> [ GDS adapters ]               (flight, date, fare_class)
            circuit-broken,                     │
            rate-limited                        └──> [ ticketing / accounting ]

   background: revenue management moves seats between buckets
               sweeper releases expired holds
               irregular-ops rebooking queue, one writer per flight

What an interviewer will push on

  • “How do you prevent selling the same seat twice?” — a conditional update against the fare-class bucket for inventory, and a composite primary key for specific seat assignments. Then say that overbooking means the goal is not zero oversell.
  • “Why not just count seats?” — because fare classes are the product. Availability is per bucket, and the buckets move.
  • “How do you hold a seat during payment?” — a held counter plus an expiring hold row; treat expired holds as released at read time and let the sweeper tidy up.
  • “Search is too slow.” — precompute route structures offline, cache by origin-destination-date, and accept that quoted prices are indicative and re-validated.
  • “The payment succeeds but ticketing fails.” — a saga with compensations; void the ticket, and never leave money captured against nothing.
  • “How do you scale it?” — shard inventory by flight and date, which co-locates every row a booking must compare. Search scales independently and is almost entirely cache.
  • “A storm cancels 400 flights.” — automated prioritised rebooking through a per-flight queue, plus deliberate degradation of everything that is not rebooking.

What the three case studies have in common

Worth ending the case studies by putting them side by side, because the same question gets three different correct answers.

AirbnbAmazonAirline
The unitA date rangeA countA count, per fare bucket
GuardExclusion constraintConditional updateConditional update
Perishable?No — resell tomorrowNo — restockYes, absolutely
Oversell?NeverDeliberately, sometimesDeliberately, always
If inventory is downRefuseAccept and reconcileRefuse
Peak multiplierx3x10 (Black Friday)x10+ (disruption)
Write rate at peak0.3/sec200/sec1/sec

Every one of those write rates is trivial, and every one of these systems is hard. That is the single most transferable observation in the three posts: in transactional systems, the difficulty is almost never throughput. It is correctness under concurrency, and what happens when a dependency you do not control is unavailable.

The perishability row is what makes this one different. It is why airlines oversell, why they price the same seat five ways, and why a storm is an engineering problem rather than only an operational one — an unsold seat on a departed flight is the one loss the system can never recover.

Next, and last: the interview questions — everything in this track, condensed into answers short enough to say out loud.