Design a Flight Booking System
Run this for someone else. You hold the answers; they do not. Read the prompt, keep the clock, and use the probes below when an answer is thin. Do not show them this page.
Open with this
Answer 50 M searches a day from 20 slow, metered suppliers through a fare cache, then book seats you do not own, exactly once. Take a couple of minutes on requirements, then we will do some numbers, then the design. I will interrupt to keep us moving.
The clock
- 4 min: functional requirements and scope
- 4 min: non-functional requirements, with numbers
- 5 min: back-of-envelope estimates
- 16 min: high-level design and one or two flows
- 16 min: deep dives and the close
Move them on out loud when a section overruns. The commonest failure is spending twenty minutes on requirements and never reaching a deep dive, and preventing that is your job as much as theirs.
Requirements · 8 min
Listen for: a scoped set of capabilities, an explicit out-of-scope list, and numeric targets rather than adjectives. Prompt with “what are you not building?” if they never scope, and “what number would make that requirement real?” if they say “fast” or “highly available”.
Functional (8)
- Search flights: Origin and destination as airports or cities (London is six airports), one-way or return, local departure dates, cabin, and the party (adults, children, infants). Every result is priced for the whole party, taxes included.
- Results in seconds, streamed: Cached results in under a second, then more as suppliers answer, until a cut-off at 4 to 5 seconds. Sort and filter by price, duration, stops and times; the same flights sold by several suppliers appear once.
- Pick an itinerary and re-check the price: The price in the results is a hint. Picking one re-prices it live with the supplier that sells it, and a changed price or a sold-out fare is shown before any passenger or card details are entered.
- Book, pay and get a ticket: The airline reservation, the card charge and the ticket either all happen or are all undone. The confirmation carries our booking reference, the airline locator and the e-ticket numbers.
- View, change or cancel a booking: Change to another flight for the fare difference and any fee, or cancel: a void inside the void window, otherwise a refund under the fare rules.
- Airline schedule changes: Airlines retime, renumber and cancel flights after we sell them. The booking is updated, and the traveller is told and offered what the airline allows: accept, another flight, or a refund.
- Mixed one-way fares (optional): A return trip built from two one-way tickets on different airlines when that is cheaper, clearly labelled as two separate tickets.
- Out of scope: Seat selection and ancillaries beyond the baggage a fare includes, loyalty, hotels and packages, multi-city trips, fraud scoring, and the payment system itself (see Design a Payment System). We integrate with distribution systems; we do not build one.
Non-functional (8)
- Time to results (first p95 < 1 s · final ≤ 5 s): A traveller looking at an empty page for five seconds leaves, yet the slowest suppliers take five seconds. So the first results come from the cache and live answers stream in behind them.
- Supplier calls per booking (≤ 2,000): The requirement that drives the hardest tradeoff. Every search that reaches a supplier is a paid, rate-limited call and only 1 search in 1,000 becomes a booking, so freshness bought with live calls is paid for out of bookings. Without a cache it would be 8,000 calls per booking.
- Price honesty (never charge an unverified price): A cached fare is a hint; what we charge is a price the supplier confirmed minutes earlier. Cached prices should match the live price at 90 % of price checks or better, and that rate is measured, not assumed.
- Booking correctness (one reservation · no ticket unpaid): Every attempt ends either ticketed and charged, or cancelled with the card released. A supplier timeout is an unknown, never a failure, because a blind retry books the travellers twice.
- Booking latency (ticketed p95 < 30 s): Reserve, authorise, ticket and capture are four external calls of 1 to 10 seconds each. The traveller watches a progress page; anything slower finishes asynchronously and is confirmed by email.
- Availability (search 99.9 % · booking 99.95 %): Search degrades rather than fails: a supplier that is down or out of quota is shown from its cached answers, labelled with their age, or left out. Booking may slow down but never guesses.
- Scale (50 M searches/day · 3 k/s peak): Twenty suppliers and about 50 k bookings a day. Search volume is the cost; booking volume is tiny.
- Schedule change notice (< 15 min after it reaches us): A retime that breaks a connection must reach the traveller while alternatives still exist. When the airline sends the change is up to the airline.
Estimates · 5 min
Ask for two or three numbers, not all of them. What matters is whether they state assumptions, round sensibly, and say what the number implies. Push once with “where did that come from?”
The numbers (8)
- Searches per second: ~580 avg · 3,000 peak. 50 M searches a day ÷ 86 400 ≈ 580/s. Evening peaks run about 5× the average, 3,000/s, and that peak sizes everything on the search side.
- Bookings per second: ~0.6 avg · ~3 peak. One booking per 1,000 searches is 50 k bookings a day ÷ 86 400 ≈ 0.6/s, about 3/s at peak. One Postgres primary is idle at this rate: booking is a correctness problem, not a throughput one.
- Supplier calls without a cache: ~4.6 k/s · 24 k/s peak. A search goes only to the suppliers that sell the route, about 8 of the 20 (the GDSs plus the airlines flying it). 580 × 8 ≈ 4.6 k calls/s, 3,000 × 8 = 24 k/s at peak, 400 M a day. At an assumed $0.001 a call that is $400 k a day, and the peak is many times what suppliers will accept.
- Supplier calls with the cache: ~960/s · 83 M/day. If 90 % of (search, supplier) lookups are answered from the cache, live misses are 10 % × 4.6 k ≈ 464/s, plus a fixed 500/s to refresh popular entries before they expire: ~960/s, 83 M a day, $83 k a day. The cache is not an optimisation here; it is the business model.
- Supplier calls per booking: ~8,000 → ~1,700. 400 M ÷ 50 k = 8,000 calls per booking without a cache, about $8 of search cost on every sale; 83 M ÷ 50 k ≈ 1,670 with it, about $1.70, inside the 2,000 target. Price checks add ~50 per booking (one pick per 20 searches), small next to search.
- Fare cache size: ~320 GB. About 10 M distinct search keys live at a time, each with an entry per supplier (× 8), each the top 50 itineraries at ~80 B compressed: 10 M × 8 × 4 KB ≈ 320 GB before replication. The full key space (city pairs × dates × trip lengths × cabins × parties, about 13 B combinations) is never materialised; only what travellers ask for is cached.
- Open search streams at peak: ~15 k. Little’s law: 3,000 searches/s × 5 s per stream ≈ 15 k server-sent event streams open at once. Small for a stateless fleet, as long as no server holds a thread per stream.
- Live price checks: ~30/s avg · ~150/s peak. About 1 in 20 searches leads to a pick, and each pick is one live pricing call to the supplier that sells it: 580 ÷ 20 ≈ 29/s, 3,000 ÷ 20 = 150/s at peak. Few, but they must get through even when search has used up a quota, so they get a reserved lane.
High-level design · 16 min
Let them draw. Interrupt only to ask what backs a component or what a box actually does. Then pick one flow below and ask them to walk it end to end.
Components (14)
- Traveller app: Web and mobile. Opens a search as a server-sent event stream and renders results as they arrive, inserting better ones below what the traveller is reading instead of reshuffling the page. Shows every flight time in its airport’s local time, with +1 for next-day arrivals, and sends one idempotency key per booking attempt.
- API gateway (auth · bot limits · SSE): Authentication, routing, and rate limits per user, IP and partner. Bot defence is cost control here: a scraped search is a search we pay suppliers for, so anonymous and suspicious traffic gets cached results only. Holds about 15 k search streams open at peak.
- Search service (fan-out · merge · rank): Expands a request into cache keys (local departure dates, cabin, party, one key per supplier that sells the route), reads the fare cache, and asks the supplier gateway only for entries that are missing or expired. Streams merged results: cached ones within ~300 ms, live ones as they arrive, and a final event by 5 s at the latest. Groups the same flights from several suppliers by their operating flights and ranks by price, duration and stops.
- Fare cache (KV · ~320 GB · TTL by days out): Key: (origin, destination, departure date, return date, cabin, party, supplier). Value: that supplier’s top 50 itineraries with fares, fetched_at, a TTL and a hit counter. TTLs run from 10 minutes for flights in the next three days to 24 hours for flights more than six months out, scaled by how volatile the route has proved. Entries are hints for search, never a price we charge.
- Refresh scheduler (fixed budget · 500 calls/s): Every minute, takes the cache entries expiring in the next few minutes and ranks them by recent hits, so London to New York next weekend is re-fetched before it expires and the next traveller still gets an instant, complete answer. Spends a fixed budget of about 500 calls/s, only from quota that live traffic leaves unused, and is the first thing to stop when a supplier is busy.
- Supplier gateway (adapters · quotas · deadlines): One adapter per supplier (GDS protocols, airline NDC APIs) that normalises answers into one itinerary and fare model. Enforces each supplier’s quota with priority lanes (booking, then price checks, then live search, then refresh), per-call deadlines and circuit breakers, and coalesces identical requests in flight. It owns each call, not the caller, so an answer that arrives after its search has closed is still written to the fare cache.
- Suppliers (GDSs + airline APIs · paid): Global distribution systems (Amadeus, Sabre, Travelport) and airlines’ own APIs. They own the seats, the fares and the reservations. Shopping calls take 1 to 5 seconds, are rate-limited and paid for per request, and agency contracts have historically watched the look-to-book ratio. They also report changes later, on their own schedule: retimes, cancellations, ticket status.
- Offer service (live re-price · 20 min offers): When the traveller picks a result, re-prices that exact itinerary and fare live with the supplier that sells it, through the price-check lane. Answers unchanged, changed (old and new price) or sold out with alternatives, and stores the verified offer (fare, rules, baggage, total) with a 20-minute expiry. A booking can only start from a verified offer.
- Booking service (saga orchestrator): Runs each booking as a saga: reserve with the airline, authorise the card, issue the ticket, capture, with cancel, void and release as compensations. Writes each step’s intent before the call and its outcome after, so a crashed worker resumes from the record. A timeout is an unknown, resolved by looking the reservation up by our own reference before anything is retried. The only writer of booking records, including changes and cancellations.
- Payment provider (authorise · capture · void): In this design we are the merchant of record: we charge the traveller and settle with airlines through the industry settlement plans (IATA’s BSP, ARC in the US). Authorise before ticketing, capture right after, void the authorisation if no ticket is issued. Idempotency keys are derived from our booking reference and the step. See Design a Payment System.
- Booking DB (Postgres · states · outbox): The source of truth for what we sold, what we told the traveller and what money we hold: booking reference, state, version, passengers, segments (local times, IANA zones and UTC instants), supplier locator, ticket numbers, ticketing deadline, payment ids and a step log. Every transition is a conditional update plus an outbox row. 50 k bookings a day at ~10 KB is under 200 GB a year: one primary with a synchronous replica.
- Event bus (Kafka · key = booking ref): Booking events relayed from the outbox, keyed by booking reference so one booking’s events stay in order: confirmed, failed, schedule changed, cancelled, refunded. Consumers: the notifier, reconciliation against suppliers’ ticket reports, and analytics.
- Notifier (email · push · SMS): Sends the confirmation with e-ticket numbers, schedule-change notices with the options the airline allows, failures with what happened to the money, and refund receipts. Deduplicates on event id, so a replayed event never sends a second email. Renders every time in its airport’s local time from the stored zone, never the server’s or the reader’s.
- Schedule change processor (GDS queues · NDC notices): Assume one booking in ten sees an airline change before it flies: about 5 k changes a day. Drains the GDS queues where changed reservations land every few minutes and accepts airlines’ order-change notifications, maps each to a booking by supplier locator, and has the booking service re-read the reservation and apply the difference. Treats every notice as a pointer, never as the data.
Flows to ask them to walk (5)
- Search: London to New York, return, 2 adults: The read path, and most of the cost. Cached answers stream back in under a second, live suppliers fill in behind them under a per-supplier deadline, and nobody waits for the slowest supplier.
- The traveller searches; the app opens a result stream.
- The search service turns the request into one cache key per supplier that sells the route.
- It reads the fare cache: six of the eight supplier entries are fresh.
- For the other two, it asks the supplier gateway with a deadline of 4 seconds from the start of the search.
- The gateway calls the two suppliers and normalises their answers.
- Each answer is written to the fare cache and merged into the stream.
- One supplier misses the deadline; the stream completes without it.
- Pick a flight, re-check the price, book it: The write path. The cached price is verified with the supplier before any card is touched, then the booking runs as a saga across the airline and the payment provider, each step recorded before it is called.
- The traveller picks a result; the offer service re-prices it live.
- The price has risen; the traveller sees the old and new total and decides.
- The traveller accepts and books; the booking service records the attempt.
- Step one: create the reservation with the airline, carrying our reference.
- Step two: authorise the card for the verified total.
- Step three: issue the tickets.
- Step four: capture the payment and confirm.
- The confirmation goes out with the locator and e-ticket numbers.
- Evening peak: 3,000 searches a second: The scale-breaking case is not our servers but the suppliers’ quotas. The cache absorbs most of the peak, refresh gives way first, and a supplier out of quota degrades to labelled stale answers instead of errors.
- Ahead of the peak, the refresh scheduler re-fetches popular entries just before they expire.
- At 3,000 searches a second, the cache answers 90 % of supplier lookups.
- A mid-size airline’s API allows 20 calls a second, and its routes are in 10 % of searches.
- Searches that get no token show the airline’s expired entry with its age, or its flights through a GDS.
- Each gateway instance spends a leased share of every supplier’s quota.
- When the peak passes, refresh resumes and catches up on the airline’s stale entries first.
- The airline times out mid-booking: The failure path that decides whether travellers are booked twice. A timeout is an unknown; the booking service finds out what happened by our own reference before it does anything else, and undoes completed steps in reverse when a later one fails.
- The create-reservation call to a GDS times out after 30 seconds.
- After a short wait, the booking service looks the reservation up by our reference.
- It is found; the saga adopts the locator and authorises the card.
- Ticketing is then rejected: the airline no longer accepts the fare.
- Compensation: cancel the reservation and void the card authorisation.
- The traveller is told, and offered a fresh search for the same trip.
- The airline moves a flight: The asynchronous path. Weeks after selling London to San Francisco via New York, the airline retimes the first flight; the change has to reach the booking record and the traveller, and a connection it breaks has to be caught in local airport time.
- The airline retimes a flight, and the changed reservation lands on our GDS queue.
- The processor asks the booking service to re-read the reservation.
- The diff shows a three-hour retime that breaks the connection.
- The booking service writes a new version of the booking, awaiting the traveller.
- The traveller is notified with the new times and the options.
- The traveller picks a later connection; the booking service rebooks it with the airline.
Deep dives · 16 min
Pick two. Ask the headline question, let them answer, then use the follow-ups. The follow-ups are where the level gets decided, so leave time for at least three of them.
Fan-out without waiting for the slowest
Ask: Eight suppliers per search, and some take five seconds. How do you show results in under a second without giving up on the slow ones?
Good answers name: Wait for every supplier, then return one page, One fixed deadline, then a single response with whatever arrived, Stream: cached results first, live answers as they arrive, a per-supplier deadline, late answers kept in the cache, Cache only: never call a supplier from the search path.
Our pick: Each search is one server-sent event stream. The search service reads every (key, supplier) entry in one multi-get and streams the fresh ones at once, asks the supplier gateway only for the missing or expired ones with a deadline of 4 s from the start of the search, and ends with a complete event that lists any supplier that did not answer. The gateway owns each supplier call, so an answer that arrives after the search closed still lands in the cache. Results are merged as they arrive: grouped by operating flights, so the same flights from three suppliers are one row with the cheapest offer on top and the others kept as fallbacks; ranked by a best score of price plus a cost per hour of duration and per stop, with dominated itineraries (dearer, longer and with more stops than another) dropped. New results are inserted below the first screen rather than reshuffling it. Supplier calls are never hedged.
- How do you know two results from different suppliers are the same flights?
Build an identity from the segments: for each one the operating carrier, operating flight number, local departure date and the two airports, in order. Codeshares make the marketing flight number unreliable, because one physical flight can be sold under three airlines’ numbers, so operating details win where the supplier provides them. Within one identity, different fare brands (basic with no bag, standard with one) are different products and stay separate; the same brand from two suppliers collapses to the cheaper, with the other kept as a fallback for the price check. - Why not hedge slow supplier calls the way a search engine does?
A hedge is a second paid request against the same quota, and it targets exactly the supplier that is already slow or busy. At 24 k calls a second of uncached peak demand, even 5 % hedging would be 1,200 extra paid calls a second. The deadline already caps what a slow supplier can cost the traveller, and the cache turns the late answer into a hit for the next one. - A GDS is having a bad day and half its calls time out. What happens?
Its circuit breaker in the gateway opens once errors pass a threshold over a short window, and while it is open the gateway answers no capacity at once instead of spending deadline and quota. Searches show that supplier’s cached entries with their age, and the same flights mostly still arrive from the other GDSs and the airlines’ own APIs. A trickle of probe calls tells the breaker when to close. - The cheapest result has two stops and takes 19 hours. Is it first?
Not by default. The default order is a best score: price plus a value of time per hour and a penalty per stop, with the weights tuned per market from what travellers actually book, and dominated results dropped outright. Cheapest and fastest stay as explicit sorts, because some travellers really do want the 19-hour flight for £80 less. - How would you know the streaming is working?
Time to first result and time to complete, as distributions, and the share of searches that close with a supplier missing, per supplier. Also how often the best result changes after the first screen: if late suppliers often beat the cached best on a route, that route’s entries are expiring too slowly or the wrong suppliers are being refreshed.
The fare cache and the supplier budget
Ask: Fares change all the time. Why is caching them safe, how long should an entry live, and what does the cache actually save?
Good answers name: No cache: every search calls every relevant supplier live, One TTL for every entry, say 30 minutes, TTL by days to departure × a learned route factor, plus refresh of popular entries inside a fixed call budget, Precompute every route and date in a nightly batch.
Our pick: Each entry is keyed by (origin, destination, departure date, return date or one-way, cabin, party, supplier), with dates as local departure dates and the party normalised. The value is that supplier’s top 50 itineraries, fetched_at, a TTL and a hit counter: about 4 KB, 10 M live keys, ~320 GB in a sharded in-memory or SSD-backed KV store such as Redis Cluster or Aerospike. The base TTL comes from days to departure: 10 minutes within 3 days, 30 minutes within 2 weeks, 2 hours within 2 months, 8 hours within 6 months, 24 hours beyond. It is multiplied by a route factor between 0.25 and 2, learned from price checks: each pick compares the cached price, and its age, with the live one, and the factor is tuned so at least 90 % still match. A refresh scheduler spends a fixed 500 calls/s re-fetching the most-hit entries just before they expire, from quota live traffic leaves unused, and stops first under pressure. The result is ~90 % of lookups from the cache, ~960 calls/s instead of 4.6 k, and about 1,700 calls per booking instead of 8,000.
- Your 90 % hit rate is a guess. What if it is 60 %?
Then misses are 40 % of 4.6 k, about 1,860 calls a second, plus 500 for refresh: roughly 2,350 a second, 200 M a day, about 4,000 calls or $4 per booking. Still half the cost of no cache but twice the target, so I would find where the misses are; usually the long tail of rare routes, unusual parties and far-off dates. Longer TTLs for far-off dates and pricing several party sizes from one entry (for suppliers that return per-passenger fares with seat counts) move the hit rate most for the least staleness. - An airline launches a flash sale at noon and fares on a route drop 40 %. The cache shows the old prices for hours.
That is the costly direction of staleness: we look dearer than a competitor that searched live. Price checks see the drop at once and write it back, and a large mismatch on a route sharply cuts that route’s factor and queues its popular entries for refresh. Where we can get fare-filing updates for a route, a change can expire its entries directly. Neither is instant, which is the honest price of caching. - A cache node fails at peak and takes a tenth of the keys with it.
Those keys miss, so for a while about 2,200 extra calls a second go live on top of the 2,400 already missing. The gateway’s quotas cap what reaches each supplier, so the excess becomes fewer live results and more labelled stale ones, not an outage. Replicas make it rare. The point is that the cache is a cost and latency layer, never a correctness one: losing it costs money, not bookings. - Isn’t the refresh scheduler spending calls to avoid spending calls?
It does not save calls on the entries it refreshes, it moves them. A popular entry would be re-fetched by the first traveller to miss it anyway; refreshing it a minute early takes that call off the traveller’s critical path and onto quota nobody is using. It only wastes calls on entries that would not have been hit before expiring again, so it ranks by recent hits and stops below about one expected hit per TTL. It is the same family as the check scheduler in Design a Price Tracker, simpler because demand is the whole signal. - How do you cache "nothing available on that date"?
As an entry like any other, with its normal TTL: a sold-out answer is information, and without it every search for a full flight costs a call that returns nothing. Whether a route is flown at all is a different question, answered by the schedule-derived supplier table, which changes rarely and is rebuilt when schedules change.
Cached fares are hints
Ask: The results said £1,224. The traveller clicks, and the airline now wants £1,284. What happens, and why not just book at the cached price?
Good answers name: Re-price live when a result is picked; show any change before passenger and card details, Book at the cached price and absorb the difference, Verify only when the booking is submitted, Hold seats with the airline as soon as a result is picked.
Our pick: A pick sends the signed result token to the offer service, which makes one pricing request for that exact itinerary and fare to the supplier that sells it, in the price-check lane, whose reserved quota means a busy evening of searches cannot crowd it out. The answer is unchanged; changed, with the old and new totals side by side and an explicit accept; or sold out, in which case the same flights from another supplier (kept by the merge) are offered first, then the next results of the same search. A price that fell is shown and charged at the lower figure. The verified offer is stored for 20 minutes; booking starts only from an offer and re-prices it if it has expired. Every check is written back to the fare cache and its mismatch, by age, feeds the route’s TTL factor, so the cache becomes more accurate exactly where travellers notice.
- Why not hold the seats when the traveller picks, so the price cannot change while they type?
One search in 20 leads to a pick and there are 1,000 searches per booking, so about fifty picks happen for every sale. Holding on pick would create some fifty reservations for each one we keep, which airlines treat as speculative booking: it ties up their seats and can bring penalties. A 20-minute verified offer gets most of the benefit, because prices rarely move again within minutes of a live check. - The offer expires while the traveller is typing passport details.
The booking service re-prices it on submit. Same price, and the traveller never notices. A different price, and the booking stops before reserving and shows the change, keeping everything they typed. Twenty minutes is long enough for most checkouts and short enough that the re-check is rarely needed. - How do you stop someone editing the price in the result token?
The token is signed by the search service and carries the supplier, fare key, itinerary and displayed price. The offer service checks the signature and then ignores the displayed price for charging anyway: it prices from the supplier, and the displayed price only decides whether to show "changed". The client never chooses what it pays. - What does the mismatch rate tell you?
It is the measured staleness of the cache, per route and per days-out band. Above the 10 % target, a route’s TTL factor drops; near zero, it rises and the route costs fewer calls. It also catches broken adapters: a sudden jump for one supplier across every route usually means its fare parsing changed, not the market. - The traveller searches in euros but the airline prices in pounds.
Store and verify in the supplier’s currency. Either show a labelled estimate and charge in the supplier’s currency, or lock a conversion rate into the offer and charge in the traveller’s, valid for the offer’s 20 minutes. The amount to authorise is computed once, at the offer, in integer minor units, and the booking stores both amounts.
Booking as a saga across external systems
Ask: Reserve with the airline, charge the card, issue the ticket: three systems, none in your transaction. In what order, and what happens at every failure?
Good answers name: Orchestrated saga: reserve, authorise, ticket, capture; each step’s intent and outcome recorded; compensations in reverse, Charge first, then reserve and ticket, Choreography: each step reacts to the previous step’s event, The airline charges the card itself during ticketing.
Our pick: The booking service orchestrates, with the booking record as its state: created → reserving → reserved → authorising → authorised → ticketing → ticketed → capturing → confirmed, plus reserve_unknown and ticket_unknown for timeouts and cancelled, voided, refunded and needs_review as endings. Each step writes its intent with a conditional update before the call and its outcome after, and every external call carries an idempotency key derived from our booking reference and the step. A decline at authorisation cancels the reservation. A ticketing failure cancels the reservation and voids the authorisation. A capture failure is retried (an authorisation lasts about a week) but must be resolved inside the void window: if the card cannot be charged, the tickets are voided, the reservation cancelled and the traveller told. Capture runs straight after ticketing so that window is almost never tested. Reconciliation compares the suppliers’ ticket reports with our ticketed bookings and captures every hour, while a void is still possible, and our captures with the payment provider’s settlement nightly.
- The worker crashes right after the tickets are issued, before it records them.
The booking stays in ticketing. A recovery sweep finds bookings in an in-flight state longer than that step’s timeout and reads the reservation by locator: it shows ticket numbers, so the record moves to ticketed and the saga continues to capture. Ticketing is never repeated without that read, for the same reason the create is not: a second ticket is a second sale. - Why authorise after reserving rather than before?
The reservation is where most failures happen (sold out, price changed), and it is free to cancel. Authorising first would put a hold on the card for every booking that then fails to reserve, and voided holds still show in banking apps for days. The reservation also proves the exact total we are about to authorise. - You sell a return trip as two one-way tickets on two airlines. What changes?
The saga makes two reservations and issues two tickets, and the compensation for the second failing is to void the first. I would reserve the scarcer leg first, ticket both back to back and capture once both exist. The bigger risk comes after the sale: the airlines do not know about each other, so if the outbound is cancelled the return airline owes nothing. That is why it is labelled two separate tickets, shown only when the saving is real, and never used for a connection without a long layover. - The ticketing time limit is 30 minutes away and the payment provider is down.
We cannot authorise, so we cannot ticket, and the airline will cancel the reservation at the limit. Rather than let it lapse silently, the booking is cancelled with a clear message once the provider has been unavailable for a few minutes, and the traveller can retry from the same search when it recovers. We never ticket hoping the charge will work later. - How do you know no booking is stuck half-way?
A sweep alerts on any booking in an in-flight or unknown state beyond its step timeout, and on the age of the oldest one. The hourly reconciliation compares three lists: tickets in the suppliers’ reports, our ticketed bookings and our captures. A ticket without a capture, or a capture without a ticket, becomes a case the same hour, while it can still be voided.
Supplier timeouts and never booking twice
Ask: The create-reservation call timed out after 30 seconds. Do you retry it?
Good answers name: Treat the timeout as unknown, look the reservation up by our reference, and retry the create only once it is confirmed absent, with the same reference, Retry on timeout with exponential backoff, Treat the timeout as a failure and tell the traveller, Cancel whatever might exist, then start again.
Our pick: Our booking reference is generated before the first call and written into every reservation we create, in whatever field the supplier offers for an agency reference. A create that times out moves the booking to reserve_unknown; after about 30 seconds the booking service looks the reservation up by that reference, retrying the lookup with backoff for a few minutes. Found: adopt the locator with a conditional update (reserve_unknown → reserved), so a late original answer cannot apply a second time, and continue. Not found after that window: retry the create with the same reference, at most twice, then fail cleanly. Ticketing follows the same pattern, reading the reservation for ticket numbers before any retry. For a supplier that cannot find by reference, the lookup falls back to the agency’s recent reservations filtered by passenger names, flights and date, and the hourly reconciliation flags any duplicate that slips through so it is cancelled before it is ticketed.
- The lookup itself times out.
Stay in reserve_unknown and keep looking with backoff for a bounded window, say ten minutes; lookups are safe to repeat. If the window passes, the booking goes to needs_review and the traveller sees "we are confirming your booking", never "failed", because failing invites a second booking while the first may exist. The airline’s time limit bounds the damage if it does. - Two workers pick up the same booking after a crash and both want to retry.
Each must move the booking out of reserve_unknown with a conditional update before calling the supplier, so one wins and the other sees the new state and stops. And both would send the same reference, so even a race that slipped past would appear as two reservations with one reference, which the lookup and the reconciliation both catch. - Why is the reference ours rather than the airline’s locator?
The locator only exists once the supplier answers, and the case that matters is the one where it did not. A reference we generate before the call names the attempt whatever happened to the response, the same idea as a client-generated idempotency key for a payment. - Could a duplicate still reach the traveller?
Only if a lookup misses a reservation that exists. That is why reconciliation pulls each supplier’s reservations and tickets for our agency and groups them by our reference and by passengers and flight. Two reservations for one booking get the unticketed one cancelled at once; two tickets get the newer one voided, which is why it runs hourly, inside the void window.
The booking record, schedule changes and local time
Ask: The airline owns the flights. What is your booking record the source of truth for, and how do airline changes reach it?
Good answers name: Our own versioned booking record with explicit states; airline changes applied as new versions after re-reading the reservation, No record of our own: read the airline’s reservation every time, Apply each change notice’s contents directly, Re-read every upcoming booking’s reservation on a schedule.
Our pick: The booking record holds our reference, state, version, prices paid, payment ids, supplier locator, ticket numbers and a list of segment versions. Each segment stores local departure and arrival times with the airports’ IANA zones, plus UTC instants computed from them; durations and connections use the instants, which are recomputed if a zone’s rules change. The schedule change processor drains GDS queues every few minutes and accepts airline order-change notifications, treats each as a pointer, and has the booking service re-read the reservation and diff it. A difference becomes a new segment version under a version check and is classified by the airline’s policy: a small retime is a notice; a large one, a cancelled flight or a broken connection (the new arrival leaves less than the airport’s minimum connection time) moves the booking to change_pending with the options the airline allows. Bookings departing within 72 hours are also re-read on a schedule, as a backstop for notices that never came.
- Why store both the local time and UTC? Isn’t UTC enough?
UTC is enough for ordering and durations, but the airline, the ticket and the traveller all speak local time, and for a future flight the local time is the stable fact: if a country changes its daylight saving rules before the flight, it still leaves at 11:20 local and the UTC instant moves. So the local time and zone are the truth, and the instant is derived and recomputed when the time zone database changes. - What date is an overnight flight on?
The local departure date at the origin airport, which is what the traveller types and what the cache key uses. A flight leaving Los Angeles at 23:50 on the 18th is a flight on the 18th even though it lands on the 19th. Results show the arrival with +1, and a filter like "arrives before noon" is evaluated in the arrival airport’s zone. - The airline sends the same change notice three times, then a newer one before the second.
A notice only triggers a re-read, so duplicates cost a supplier read and change nothing: the diff against the current version is empty. Order does not matter either, because whichever read happens last sees the airline’s current reservation. The version check on our write stops two processors applying the same diff twice. - The traveller never responds to the change.
Reminders escalate as departure approaches (email, then push, then SMS, then a call from support for a broken connection in the last day). If nothing happens, the airline’s own rules apply at the airport, and many airlines rebook a broken connection themselves. We never accept a significant change on the traveller’s behalf, because accepting it can give up their right to a free refund. - A traveller cancels a ticketed booking a month before departure.
That is a saga too: cancel at the airline (a void if still inside the void window, otherwise a refund request under the fare’s rules, which may keep a penalty or return only taxes), refund the card for what the airline returns minus any fee of ours, then mark the booking refunded. Airline refunds can take days, so the booking sits in refund_pending and the traveller can see what is coming back.
Close · 5 min
Ask what breaks first at ten times the load, and what they would build next. Then give them your read: one thing that was strong, one thing that was missing, one thing to practise. Be specific; “good job” helps nobody.