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 instrumentThe 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/secOne 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 - heldNote 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 incidentEngineering-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 rowThis 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 reissuedA 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 forfeitTwo 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 fails | Effect | Degradation |
|---|---|---|
| Search cache | Every 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 partner | That airline’s flights disappear from results | Circuit-break it and show the rest. Partial results beat an error page. |
| Inventory service | No holds, no bookings | No safe degradation — unlike retail, you cannot optimistically accept and reconcile, because a seat cannot be manufactured. Refuse cleanly. |
| Payment | PNRs created, unticketed | They expire naturally by the ticketing deadline, releasing the seats. The domain already has this concept, which is convenient. |
| Ticketing | Payment 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 flightWhat 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.
| Airbnb | Amazon | Airline | |
|---|---|---|---|
| The unit | A date range | A count | A count, per fare bucket |
| Guard | Exclusion constraint | Conditional update | Conditional update |
| Perishable? | No — resell tomorrow | No — restock | Yes, absolutely |
| Oversell? | Never | Deliberately, sometimes | Deliberately, always |
| If inventory is down | Refuse | Accept and reconcile | Refuse |
| Peak multiplier | x3 | x10 (Black Friday) | x10+ (disruption) |
| Write rate at peak | 0.3/sec | 200/sec | 1/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.