Design flight search and booking (MakeMyTrip)
The interviewer asks
“Design flight search and booking for an online travel agency like MakeMyTrip. Users search one-way or round-trip flights across many airlines, see prices in a couple of seconds, and book with payment. Airline data comes from GDSs and airline APIs that are slow, rate-limited and charge per search.”
What a strong answer looks like
A strong answer starts from the hard constraint: we do not own the inventory. Fares and seats live in GDSs (Amadeus, Sabre, Travelport), NDC APIs of full-service airlines and direct APIs of low-cost carriers like IndiGo, which are slow (1-5 s), rate-limited, and often charge per request, while the look-to-book ratio is in the hundreds or thousands. So search is a fan-out to suppliers with strict timeouts plus an aggressive fare cache keyed by (origin, destination, date, pax, cabin) with short TTLs, and the price shown is a cached quote. Booking is a different path: re-price and re-validate the chosen itinerary live, hold the seat (create a PNR), take payment, then issue the ticket, as a durable saga with compensations, because payment success with ticket failure is common and must be handled. Deep dives: caching vs freshness and the price-change-on-booking problem, the booking saga with PNR/ticketing failures, and fan-out/ranking under supplier latency.
Concepts to mention
1 Requirements
Functional
- Search one-way, round-trip and multi-city flights by origin, destination, dates, passengers and cabin.
- Show results with price, duration, stops and baggage; filter and sort; show fare families (saver, flexi).
- Select an itinerary; re-price it live and show any price change before payment.
- Collect passenger details, add-ons (seats, meals), and take payment.
- Create the booking (PNR) with the airline and issue e-tickets; send itinerary by email/WhatsApp.
- Cancellations and refunds per fare rules; handle airline schedule changes.
Non-functional
- Search results in under 2-3 s p95, with first results streaming earlier.
- Price shown at search should match the live price for at least ~95% of booking attempts.
- Booking success: never charge without either a ticket or an automatic refund.
- Respect supplier rate limits and keep per-search supplier cost under budget.
- 99.95% availability for search; booking path survives supplier timeouts.
- Handle holiday peaks (Diwali, summer vacations, long weekends) at 3-5x normal search traffic.
Out of scope (say it out loud)
- Hotels and holiday packages.
- Fare rules engine internals of airlines.
- Loyalty program accounting.
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
POST/v1/flights/search | Start a search {legs, pax, cabin}; returns search_id and cached results immediately. |
SSE/v1/flights/search/{search_id}/stream | Stream more results as suppliers respond, until done or timeout. |
POST/v1/flights/reprice | Live re-price of a selected itinerary {offer_id}; returns confirmed price or the new price. |
POST/v1/bookings | Create booking {offer_id, passengers, add-ons}; Idempotency-Key; returns booking_id and payment intent. |
GET/v1/bookings/{id} | Booking status: pending_payment, held, ticketing, ticketed, failed_refunded. |
POST/v1/bookings/{id}/cancel | Cancel per fare rules and start refund. |
4 Data model
fare_cacheRedis clusterkey = route:date:pax:cabin:supplier → offers (compressed)fetched_atttlPer-supplier entries so one slow supplier does not invalidate others. TTL depends on days to departure and route volatility (5 min for tomorrow, hours for 3 months out).
search_sessionsRedissearch_id → requestoffer_idssupplier statusesLives ~30 min; offers referenced at re-price and booking.
bookingsPostgresbooking_id (PK)user_idoffer snapshotpassengers (encrypted)amountcurrencystatuspnrsupplierticket_numbersidempotency_key (unique)created_atStrong consistency and transactions; state machine with every transition recorded.
booking_eventsPostgres (outbox) → Kafkaevent_idbooking_idtypepayloadtsDrives notifications, accounting and analytics.
search_logsKafka → data lakesearch paramsresults shownlatency per supplierclicksbookingsUsed to tune TTLs, pre-fetch popular routes and train ranking.
airports / routesPostgres + in-memoryIATA codecityaliasesnearby airportsconnections graphSmall, read-mostly; loaded into every search node.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Search DEL to BOM
- 1User searches DEL-BOM, 15 Nov, 1 adult, economy; search aggregator normalises the request and builds per-supplier cache keys.
- 2Cached offers that are still fresh are returned immediately in the first response (often 70-90% of results).
- 3For suppliers with a cache miss or stale entry, the aggregator calls connectors in parallel with a 2.5 s deadline; each connector respects a token bucket for that supplier's quota.
- 4Connectors map supplier formats (EDIFACT/XML from GDS, JSON from LCC APIs, NDC offers) to one internal offer model and write to the cache.
- 5New results stream to the client over SSE; the aggregator deduplicates the same flight sold via two channels (keep the cheaper, more reliable one) and ranks results.
- 6At the deadline the stream closes; late supplier answers still fill the cache for the next user.
Book the selected flight
- 1User picks an offer; re-price service calls the supplier live to confirm price and seat availability. If the price changed, the app shows the new price and asks to continue.
- 2User enters passenger details; POST /bookings with an Idempotency-Key creates the booking row (status pending_payment) and starts a Temporal booking workflow.
- 3Workflow creates a hold (PNR) with the airline where supported, so the seat is held for the payment window (typically 15-30 min for GDS, shorter or none for some LCCs).
- 4User pays; the workflow waits for the payment success signal (webhook from the gateway, verified).
- 5Workflow issues the ticket with the supplier; on success it stores ticket numbers, marks ticketed, and emits BookingConfirmed for email/WhatsApp.
- 6If ticketing fails after retries, the workflow cancels the hold and refunds the payment automatically, and support is notified.
7 Deep dives
Fare cache: freshness vs supplier cost
Live search for every request is impossible: suppliers are slow, cap our request rate, and charge or penalise high look-to-book ratios. So cache, but understand the cost of a stale price: if the user sees ₹4,500 and re-price says ₹5,200, many abandon, and trust drops. Make TTL a function of risk, not one number. Fares close to departure and on busy routes change fast, so they get 5-10 minute TTLs; fares 60+ days out change slowly and can live for hours. Learn TTLs from data: log the difference between cached price and live re-price per route and days-to-departure, and shorten TTLs where mismatches are frequent. Cache per supplier, not per full result, so one supplier's refresh does not require calling all. Use stale-while-revalidate: serve a slightly stale price immediately with a 'prices may change' hint and refresh in the background. Pre-fetch the top few hundred routes for the next 30 days during off-peak hours so popular searches are always warm. Measure two numbers continuously: cache hit ratio (cost) and price accuracy at re-price (quality); you are tuning a trade-off between them.
Booking saga: hold, pay, ticket
Booking crosses two external systems that can fail independently: the airline and the payment gateway. A database transaction cannot cover them, so use a durable workflow (Temporal or a state machine with an outbox) with explicit states: pending_payment → held → paid → ticketing → ticketed, and failure paths with compensations. Order matters. Hold the seat (create a PNR) before payment where the supplier allows it, so you do not take money for a seat that is gone. Capture payment only after the hold succeeds; some OTAs authorise first and capture after ticketing to avoid refunds. Ticketing calls are not always idempotent and can time out after succeeding at the airline. So before retrying, query the PNR status from the supplier to see whether tickets were issued; never blindly re-issue, or you may double-ticket. If ticketing truly fails, cancel the PNR and refund, and send an honest message. Every step is retried with backoff and has a timeout matching the hold expiry. A reconciliation job compares our bookings with supplier ticket reports and payment settlements daily, and flags 'paid but not ticketed' cases for urgent support action.
Fan-out, deadlines and ranking
A single search may call 5-10 sources: two GDSs, NDC for full-service airlines, direct APIs of low-cost carriers, and sometimes consolidators. Each has different latency and reliability. The aggregator gives every call the same overall deadline (e.g. 2.5 s) and uses a per-supplier circuit breaker: if a supplier is timing out, stop calling it for 30 s and serve cached data instead of slowing every search. Streaming results over SSE improves perceived speed: the first screen shows cached and fast-supplier results in under a second. Deduplicate the same flight from multiple channels by flight number, time and fare class, preferring the cheaper channel with a better ticketing success rate. Round-trip and multi-city searches multiply combinations; cap the combinations per leg and let suppliers' own combinable fares lead. Ranking is not just cheapest: a 'best' sort mixes price, duration, stops, departure time and airline reliability, and is trained on click-to-book data from the search logs.
8 Trade-offs
Hold before payment vs pay first
Holding first prevents charging for unavailable seats but some LCCs do not support holds, and holds expire. For those, take payment authorisation first, then ticket immediately and capture on success. The workflow must support both per supplier.
Short vs long cache TTL
Short TTLs mean accurate prices but high supplier cost and slower searches. Long TTLs are cheap and fast but cause price changes at booking. Use variable TTLs by days-to-departure and route volatility, tuned from re-price mismatch data.
Temporal workflow vs hand-written state machine
Temporal gives durable timers, retries and history out of the box, which suits a multi-step booking with 15-minute waits. A hand-written state machine on Postgres with an outbox works too but you rebuild timers, retries and visibility yourself.
Show partial results fast vs wait for all
Streaming results lowers perceived latency but the list reorders as better prices arrive. Many OTAs show a progress bar and keep the top of the list stable, inserting new cheaper results with a highlight.
9 Bottlenecks
Supplier quotas during holiday peaks
Search traffic triples but supplier quotas do not. Raise cache TTLs temporarily for far dates, pre-fetch hot routes, and apply per-user rate limits to stop bots and scrapers that inflate look-to-book.
Redis memory and hot keys
Popular route keys like DEL-BOM tomorrow are read thousands of times per second. Replicate hot keys or keep a small in-process cache in the search nodes for a few seconds.
Connector thread exhaustion
Slow suppliers hold connections. Use async I/O, bulkheads (separate pools per supplier) and circuit breakers so one supplier cannot consume all capacity.
10 Failure handling
Payment succeeded, ticketing timed out
The workflow queries the PNR status before retrying. If ticketed, it records the ticket; if not, it retries within the hold window, then cancels and refunds automatically. The user sees 'confirming your ticket' rather than a failure page.
A GDS is down
Its circuit breaker opens; searches use cached fares and other channels. Re-price for offers from that GDS fails cleanly and the user is offered the same flight from another channel if available.
Duplicate booking request
The Idempotency-Key on POST /bookings returns the existing booking, so the user is not charged twice for a double tap.
11 What impresses
- Starts by saying we do not own inventory and explains GDS/NDC/LCC sources and look-to-book.
- Treats cache TTL as a measured trade-off between supplier cost and price accuracy.
- Designs the booking as a durable saga with explicit compensations and a 'paid but not ticketed' plan.
- Checks PNR/ticket status before retrying to avoid double-ticketing.
- Uses per-supplier circuit breakers and bulkheads so one slow airline cannot hurt all searches.
- Mentions bot traffic and per-user rate limits to protect supplier quotas.
12 Mistakes
- Designing flight search as if we owned a seats table with locks.
- Calling every supplier live for every search with no caching.
- Using one fixed TTL for all fares.
- Taking payment and assuming ticketing will always succeed.
- Retrying ticketing blindly after a timeout.
- Waiting for the slowest supplier before showing any results.
13 Follow-ups
Lessons this design uses
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.