“Design MakeMyTrip” sounds like a shopping app: a user types two cities and a date, a list of flights comes back, they pick one and pay. The interviewer lets that run for a minute, then asks the question the toy version cannot answer. A user in Bangalore types “BLR to DEL, 12 August, 1 adult” and expects, in under two seconds, a ranked list of every way to make that trip - direct flights and connections - across 50-plus airlines, each with a real bookable price, layover, and total duration. MakeMyTrip does not own a single seat. The inventory lives behind dozens of airline APIs and Global Distribution Systems, each slow, rate-limited, priced per query, and disagreeing about what a fare costs from one second to the next.
The spine of this system is three coupled problems: aggregating live-ish fare and availability data from many external sources, assembling those into complete multi-leg itineraries, and serving the result fast and cheap while the underlying prices keep moving. Everything hard hangs off them - how you avoid calling 50 airlines on every keystroke, how you build a connecting itinerary from segments that come from different suppliers, how you cache prices that go stale in seconds, and what you do when the price the user saw in search is not the price the airline honors at booking. Let me build it properly.
Functional Requirements (FR)
In scope:
- Search flights by origin, destination, date, and pax. A user enters “BLR to DEL, 12 Aug, 1 adult” (optionally return date, cabin class, multi-city) and gets a ranked list of itineraries - direct and connecting - each with airline(s), departure/arrival times, total duration, number of stops, layover details, and a bookable price. This is the read-heavy heart of the product.
- Aggregate across 50+ airlines and GDSs. Results must span low-cost carriers (direct APIs), full-service carriers, and content reached through GDSs (Amadeus, Sabre, Travelport) and airline NDC endpoints. One search fans out across many suppliers with different protocols and latencies.
- Build multi-leg itineraries. When no direct flight exists (or a connection is cheaper), assemble valid connecting itineraries from separate segments, respecting minimum connection time, airport, and same-day/overnight rules.
- Rank and filter. Sort by price, duration, departure time, stops; filter by airline, time window, refundability, baggage.
- Show an accurate, bookable price. The price on the results page must be honored at checkout, or the user must be told clearly and early when it changes.
- Hand off to booking. Selecting an itinerary leads to a fare re-verification (repricing) and then the booking/PNR-creation flow.
Explicitly out of scope (say it so you own the scope):
- The booking/PNR and payment internals. Actual ticketing, PNR generation with the airline/GDS, seat maps, payment capture, and post-booking servicing (cancel/reschedule) are their own systems. We design search up to the point of repricing and handing a verified itinerary to booking.
- Hotels, trains, buses, holiday packages. MakeMyTrip sells all of these; we scope to flight air-search. The aggregation and caching patterns transfer, but we do not design them here.
- Fraud, loyalty/rewards, ML personalization of ranking. We assume a ranking service scores itineraries; we design the retrieval and assembly of candidate itineraries and hand them off.
- Supplier contracts and commercial deal logic. Markup, commissions, and supplier preference are a pricing-policy layer we leave a seam for.
The one decision that drives everything: this is a read-heavy aggregation-and-assembly problem over external, slow, expensive, constantly-changing inventory that we do not own. Unlike Airbnb, we hold no authoritative inventory; the truth lives in 50+ third-party systems. So the whole game is caching aggressively enough to be fast and cheap, refreshing intelligently enough to stay accurate, and reconciling honestly at book time when the cache is wrong. Search is approximate by nature; booking is where price becomes real.
Non-Functional Requirements (NFR)
- Scale: a large Indian OTA - assume ~10M DAU, tens of millions of searches per day, heavily skewed to a few hundred hot routes (BLR-DEL, DEL-BOM, BOM-BLR) and peak booking windows (weekends, festivals, sale days). Booking volume is a tiny fraction of search volume - a look-to-book ratio of hundreds to one.
- Latency: search results under ~2s end to end at p95, with partial/progressive results - show cached itineraries in a few hundred ms and stream in fresher/live ones as suppliers respond. A cheap cached-only answer should be sub-second. Repricing at select time under ~2-3s (it hits a live supplier).
- Availability: 99.9%+ on search. Search must degrade gracefully: if a supplier is slow or down, return what we have from cache and other suppliers rather than fail the whole search. No single airline’s outage blanks the results page.
- Consistency: search is eventually consistent and openly approximate - a shown fare may be stale by seconds to minutes. That is acceptable and expected in air travel. The authoritative price is established at repricing just before booking. We optimize search for freshness-good-enough, not correctness.
- Cost: first-class NFR here. GDS and airline lookups cost money and are rate-limited; the airline cares about your look-to-book ratio. Calling 50 live suppliers on every search is both too slow and financially ruinous. The design must serve the vast majority of searches from cache and spend live calls only where they pay off.
- Freshness: hot-route fares refreshed on the order of minutes; cold routes refreshed lazily/on-demand. Schedules (which flights exist) change slowly - daily. Prices change fast.
Back-of-the-Envelope Estimation (BoE)
Real numbers with the arithmetic, because they dictate where the pressure is.
Search volume (the dominant path):
10M DAU, ~40% run at least one flight search on a given day, ~6 searches/session
(users tweak dates, times, filters - each a re-search)
= 10M * 0.40 * 6 = 24M searches/day
= 24,000,000 / 86,400
≈ 280 searches/sec average
Peak (sale day / festival surge, ~10x) ≈ 2,800 searches/sec
Fan-out amplification (the hidden cost):
A naive search that queries live would fan out to ~50 suppliers per search.
280 searches/sec * 50 = 14,000 supplier calls/sec average, 140,000/sec at peak.
At GDS pricing of a fraction of a rupee to a few rupees per look, and airline
rate limits in the low thousands/min, this is impossible and unaffordable.
This single number is why the cache exists: we must serve >95% of searches
without a live supplier call.
Bookings (writes - tiny in comparison):
Look-to-book ~300:1 -> 24M searches/day / 300 ≈ 80,000 bookings/day
= 80,000 / 86,400 ≈ 1 booking/sec average, a few dozen/sec at peak.
Booking is not a throughput problem. Search fan-out and cost are.
Cache size (the fare cache):
Meaningful India+outbound market: ~1,500 airports we care about, but demand
concentrates. Model the cache as (origin, dest, date) cells for a rolling horizon.
Hot + warm routes: ~50,000 O-D pairs that get real traffic.
Departure horizon: ~330 days out.
50,000 O-D * 330 days = ~16.5M route-date cells.
Each cell = the cheapest-N priced itineraries for that day, say top 30 options
* ~1 KB each (segments, times, fare, rules) = ~30 KB/cell.
16.5M * 30 KB ≈ 500 GB of hot fare cache.
Fits across a Redis/KV cluster of a few TB comfortably; long-tail routes are
computed on-demand and cached briefly, not pre-stored.
Schedule + fare-rule data (slow-changing, bulk):
Global flight schedules (OAG/airline feeds): ~100k+ daily flights, schedules
published months out. A few GB, refreshed daily.
ATPCO fare filings + rules: tens of millions of fare records, ~tens of GB,
refreshed on a daily/intra-day filing cadence.
This is the raw material a pricing/shopping engine turns into cache cells.
Bandwidth:
Search response ~30 itineraries * ~1 KB (trimmed for the wire) ≈ 30 KB/response.
280 searches/sec * 30 KB ≈ 8.4 MB/sec average egress, ~84 MB/sec peak.
Modest; the cost is compute + supplier calls, not bytes on the wire.
The takeaways: search is high-volume and concentrated on a few hot routes; the killer is not storage but supplier-call fan-out (cost + latency + rate limits). The design budget goes into a fare cache that answers most searches without touching a supplier, an ingestion pipeline that keeps that cache fresh, an itinerary-assembly engine that builds connections without a live call per candidate, and an honest repricing step that makes the cheap-approximate search safe.
High-Level Design (HLD)
The architecture separates the online serving plane (answer searches fast from cache, build itineraries, rank, stream partial results) from the ingestion plane (pull schedules, fares, and availability from 50+ suppliers asynchronously and keep the fare cache warm) and a thin live-verify plane (reprice a selected itinerary against a real supplier just before booking). Search reads the cache; ingestion writes it on its own clock; booking closes the loop with a live check.
┌──────────────┐ search / select / book ┌────────────────────┐
│ User (web/ │──────────────────────────────────────▶│ Booking service │
│ mobile app) │◀── partial results streamed ──────────│ (PNR, payment) │
└──────┬────────┘ └─────────▲──────────┘
│ │ reprice
┌──────▼───────────┐ │
│ CDN / Edge │ static assets, popular result snapshots │
└──────┬───────────┘ │
│ │
┌──────▼───────────┐ │
│ API Gateway │ auth, rate limit, geo-routing │
└──────┬───────────┘ │
│ │
┌──────┼───────────────────── SERVING PLANE ─────────────────────┼────────┐
│ │ │ │
│ ┌────▼──────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │
│ │ Search │──▶│ Itinerary │──▶│ Pricing / Rank │ │ │
│ │ Orchestrator │ │ Assembly Engine │ │ + policy/markup │ │ │
│ │ (cache-first, │ │ (graph: direct + │ └────────┬─────────┘ │ │
│ │ fan-out mgr) │ │ connections) │ │ │ │
│ └───┬───────┬───┘ └────────┬─────────┘ │ │ │
│ │ │ │ │ │ │
│ ┌───▼───┐ ┌─▼──────────┐ ┌───▼──────────┐ ┌─────────▼────────┐ │ │
│ │ Fare │ │ Schedule / │ │ Availability │ │ Result cache │ │ │
│ │ cache │ │ graph store│ │ store (RBD) │ │ (session, snapshot)│ │ │
│ │(KV) │ │ (routes) │ │ │ └───────────────────┘ │ │
│ └───▲───┘ └─────▲──────┘ └──────▲───────┘ │ live │
└─────┼──────────┼───────────────┼───────────────────────────────── │ reprice│
│ │ │ │ │
┌─────┼──────────┼─────── INGESTION PLANE ─────────────────────────┼──────┐ │
│ ┌───┴──────────┴───────────────┴────────┐ ┌───────────────────┐ │ │ │
│ │ Fare Cache Builder / Shopping Engine │◀──│ Supplier Adapters │◀─┘ │ │
│ │ (prices itineraries offline, writes │ │ (per-supplier: │ │ │
│ │ top-N per O-D-date into fare cache) │ │ GDS / NDC / LCC) │─────────┘ │
│ └──────────────▲──────────────────────────┘ └────────▲──────────┘ │
│ │ schedules, fares, avail │ poll / webhook │
│ ┌──────────────┴───────┐ ┌──────────────────┐ ┌─────┴─────────────────┐ │
│ │ Schedule feed (OAG/ │ │ ATPCO fare filings│ │ 50+ airline / GDS │ │
│ │ airline) daily │ │ (rules, daily) │ │ external endpoints │ │
│ └──────────────────────┘ └──────────────────┘ └───────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────────┘
Search flow (the dominant, cache-first path):
- User searches “BLR to DEL, 12 Aug, 1 adult.” The Search Orchestrator normalizes the query (airport codes, date, pax, cabin) and first hits the fare cache for
(BLR, DEL, 12-Aug, ECONOMY). On a hot route this returns pre-priced top-N itineraries in a few ms. - In parallel, the orchestrator asks the Itinerary Assembly Engine whether connecting itineraries (BLR-HYD-DEL, BLR-BOM-DEL) beat or complement the direct set, using the schedule graph plus cached fares for each leg.
- The Pricing/Rank stage applies markup/commission policy, computes total price and duration, and ranks. Cached results are returned immediately as partial results.
- If the route is cold, stale, or the user is deep in a booking funnel, the orchestrator fires a bounded live fan-out to a few relevant suppliers (not all 50), with strict timeouts, and streams fresher itineraries into the open result set as they arrive. Whatever has not returned by the deadline is simply omitted; the page never blocks on the slowest airline.
- The result set is cached per session (so pagination/filtering does not recompute) and hot snapshots are pushed to the edge.
Ingestion flow (keeps the cache honest, off the request path):
- Schedule feeds (OAG / airline direct) load daily into the schedule/graph store: which flights exist, on which days, with what times and equipment. This is the skeleton the assembly engine walks.
- ATPCO fare filings and airline fare data load into the shopping engine, which continuously prices popular O-D-date combinations (base fare + rules + taxes) and writes the top-N itineraries per (origin, dest, date) into the fare cache. Hot routes are re-priced every few minutes; cold routes on demand.
- Availability (RBD / booking-class seat counts) is polled/subscribed per supplier and stored in the availability store, because a cheap fare is worthless if the fare’s booking class has no seats.
- Supplier adapters normalize each source (GDS response, NDC XML/JSON, LCC REST) into a common internal itinerary/fare model, so the rest of the system is supplier-agnostic.
Live-verify flow (makes cheap search safe):
- User selects an itinerary. The Booking service calls the owning supplier to reprice - re-verify the exact fare and availability live.
- If the price matches, proceed to payment and PNR creation. If it changed, show the new price clearly before charging. This is where the approximate cache is reconciled against truth.
The key insight: serve almost everything from a warm cache the ingestion plane maintains asynchronously, spend scarce live supplier calls only on cold routes and on the final repricing, and stream partial results so a fast cached answer never waits on a slow airline. Search is deliberately approximate; correctness is deferred to repricing.
Component Deep Dive
The hard parts, naive-first then evolved: (1) aggregating from 50+ slow, costly suppliers without dying on fan-out, (2) building multi-leg itineraries from separate segments, (3) serving with low latency and graceful degradation, (4) the look-to-book / stale-price problem and honest repricing.
1. Aggregating inventory from 50+ suppliers
MakeMyTrip owns no seats. Every price and every seat count comes from an external system - a GDS, an airline NDC endpoint, or a low-cost carrier’s REST API - each with its own protocol, latency (200ms to several seconds), rate limit, and per-call cost. How you gather this is the whole ballgame.
Naive approach: fan out to all suppliers live on every search.
on search(BLR, DEL, 12-Aug):
responses = parallel_call(all_50_suppliers, query) # wait for all
return merge(responses)
Where it breaks:
- Latency is the slowest supplier. Waiting for all 50 means the p95 response is gated by the slowest, flakiest airline. One supplier at 6s makes every search 6s.
- Cost and rate limits are fatal. From the BoE, live-everything is ~14,000 supplier calls/sec average. GDS looks cost money; airlines throttle you and punish a bad look-to-book ratio by degrading or cutting access. This is not a tuning problem, it is a business-ending one.
- Redundant work. Ten thousand users searching BLR-DEL in the same minute each trigger 50 identical live calls. The same fares are fetched over and over.
It is the obvious first design and it is unshippable at scale.
First evolution: a fare cache fronted by asynchronous ingestion. Decouple fetching fares from serving searches. A background shopping engine continuously prices popular O-D-date combinations and writes the top-N priced itineraries per (origin, dest, date, cabin) into a fast KV fare cache. A search then reads the cache - no live supplier call for the common case. Hot routes (a few hundred) are re-priced every couple of minutes; warm routes every 15-30 minutes; cold routes not at all until someone asks.
fare:{origin}:{dest}:{date}:{cabin} -> [ {itinerary, segments, fare, rules,
source, priced_at }, ... top N ]
with a per-cell TTL scaled by route heat (hot = short, cold = absent)
This alone takes the vast majority of searches off the suppliers entirely - the >95% cache hit the BoE demands.
Second evolution: normalized supplier adapters + a shared internal model. Each supplier speaks a different dialect. Wrap each behind an adapter that translates its response into one canonical itinerary/fare/availability schema, and that owns that supplier’s quirks: auth, rate-limit budget, ret/timeout policy, and a circuit breaker so a failing supplier is skipped fast rather than dragging searches down. The orchestrator and assembly engine never see supplier-specific formats.
Adapter responsibilities per supplier:
- protocol translation (GDS pricing message / NDC XML / LCC REST) -> canonical
- rate-limit token bucket (respect the airline's ceiling)
- timeout + retry + circuit breaker (fail fast, don't cascade)
- look-to-book accounting (track how many looks per booking we spend here)
Third evolution: tiered, on-demand freshness rather than one refresh rate. Not all routes deserve the same effort. Classify routes by traffic and refresh accordingly; for the long tail, do a live fetch on the first search, then cache the result briefly so the next user on that cold route is served from cache. Live fan-out, when it does happen, goes only to the relevant suppliers for that route (the carriers that actually fly it, plus the GDSs that hold their content), not all 50 - a route-to-supplier map keeps the fan-out to a handful.
The principle: never let a user search touch 50 live suppliers. An async ingestion pipeline prices popular combinations into a cache ahead of demand; adapters normalize and rate-govern each source; freshness is tiered by route heat; and live calls are a narrow, bounded exception, not the rule. This is what makes search both fast and financially viable.
2. Building multi-leg itineraries
BLR-DEL has direct flights, but many valuable results are connections - BLR-HYD-DEL, BLR-BOM-DEL - often across two different airlines. The system must assemble complete itineraries from separate segments, and a naive approach either misses them or explodes combinatorially.
Naive approach: only return direct flights each supplier gives you.
for each supplier: ask "flights BLR->DEL on 12 Aug", show what comes back
Where it breaks:
- Misses cross-airline connections. No single supplier will hand you “IndiGo BLR-HYD then Air India HYD-DEL” as one product - that itinerary does not exist in anyone’s inventory until you construct it. You lose the cheapest or fastest option on routes without good direct service.
- On thin routes there may be no direct flight at all, so “only direct” returns an empty page for a trip that is perfectly doable with one stop.
First evolution: model the network as a graph and search it. Load the schedule graph from the daily feed: nodes are airport-time events (a flight departs/arrives at a place and time), edges are flights (fly this segment) and legal connections (wait at an airport between two flights). Finding itineraries is a time-expanded graph search from origin to destination on the given date, bounded to keep it sane:
Constraints that bound the search (and keep results legal):
- max stops (usually 1, occasionally 2) # prune depth
- Minimum Connection Time (MCT) at each hub # can you make the transfer?
- max total duration / max layover (no 14h stops) # sane itineraries only
- same-day or bounded overnight # no absurd routings
- avoid backtracking / circuitous geography # heuristic prune
A bounded search (typically to depth 1-2 stops) over the schedule graph enumerates candidate itineraries: direct segments, plus connection paths through a small set of viable hubs (DEL, BOM, HYD, BLR). The MCT check is what makes a connection bookable - too tight and the passenger misses the second flight; too loose and it is not a real product.
Second evolution: separate schedule search from pricing. The graph search over schedules is cheap and returns candidate itineraries (which flights, in what order). But an itinerary’s price is not the sum of two independent one-way fares in general - fares have rules, and a connection may be priced as a through-fare or as two segments. So the assembly engine produces candidate segment combinations, then prices each candidate from the fare cache (per-leg cached fares as a fast approximation), keeping only the cheapest/fastest few per stop-count. Only survivors that a user might actually select get an authoritative price later at repricing.
1. schedule graph search -> candidate itineraries (segment sequences), bounded by MCT/stops
2. for each candidate: assemble price from cached leg/through fares (approximate, fast)
3. keep top-K by price and by duration per stop-count (dedupe near-identical routings)
4. hand ranked candidates to the serving plane; reprice the selected one live
Third evolution: precompute connections for hot routes. For the hundreds of hottest O-D pairs, the shopping engine pre-builds and pre-prices the best connecting itineraries into the fare cache alongside directs, so a BLR-DEL search returns directs and good connections with zero graph search on the request path. Cold routes run the bounded graph search on demand and cache the result briefly.
The principle: itineraries are assembled by a bounded time-expanded search over a schedule graph (respecting MCT, stops, and duration limits), priced approximately from the fare cache, and precomputed for hot routes - so connections across different airlines appear as first-class products without a combinatorial blow-up or a live call per candidate.
3. Low-latency, cheap serving with graceful degradation
Search must feel instant and cost almost nothing, over data that lives behind slow external systems. The tension is fundamental: fresh data is slow and expensive; fast and cheap data is stale.
Naive approach: synchronously gather everything, then render one complete response.
results = wait_for( cache_read AND live_fanout AND assembly ) # block on all
render(results)
Where it breaks:
- You wait for the slowest input every time, so even when 90% of the answer is a 5ms cache hit, the response is gated by whatever live supplier you also asked. The fast path is held hostage by the slow one.
- A single supplier timeout or outage blanks or stalls the whole page. All-or-nothing rendering has no graceful failure mode.
First evolution: cache-first with progressive (streamed) results. Answer immediately from the fare cache and stream improvements. The response is a live, growing set:
t=0ms : return cached top-N itineraries (fast, approximate) -> user sees results
t+X ms : bounded live fan-out to relevant suppliers, results streamed in as they land
t+deadline: close the stream; whatever hasn't returned is dropped
Over WebSocket/SSE (or long-poll), the results page paints in a few hundred ms from cache and quietly refines - a fresher price appears, a new connection slots in. The user perceives speed; freshness catches up in the background. Crucially, the deadline is a hard cap: the page never waits on a straggler.
Second evolution: degrade gracefully, never fail the whole search. Every live supplier call is wrapped in a timeout + circuit breaker. If a supplier is slow or down, its adapter returns nothing and the search proceeds with cache + the suppliers that did answer. A note - “prices for airline X may be updating” - is honest and far better than an error page. No single airline’s outage can blank the results.
Third evolution: layered caching to kill repeated work.
Edge/CDN : snapshots of ultra-hot (route, date) result pages, TTL seconds-minutes,
for anonymous top-of-funnel searches (BLR-DEL tomorrow).
Result cache : per (route, date, cabin, pax) assembled+ranked result, short TTL,
shared across users searching the same thing in the same window.
Session cache : the user's current result set, so filtering/sorting/paginating
re-renders from memory, never recomputes or re-fetches.
Fare cache : the pre-priced per (O-D, date) itineraries (Component 1) - the workhorse.
Two users searching BLR-DEL for tomorrow within the same minute hit the same result-cache entry; a returning user filtering by “non-stop only” re-slices their session cache with zero backend work. This collapses the effective supplier and compute load far below raw search QPS.
The principle: cache-first serving with progressive results and hard deadlines makes search feel instant; circuit-breakers and partial rendering make it survive supplier failures; and layered caches (edge, result, session, fare) collapse repeated work so the system is cheap. Speed and cost both come from not doing work synchronously that a cache already did.
4. The look-to-book problem and honest repricing
The uncomfortable truth of every OTA: the price shown in search is not guaranteed. It came from a cache that may be minutes old, or from a fare whose booking class just sold out. Airlines reprice constantly. If you charge the cached price and the airline honors a different one, you either eat the loss or break trust. This is the look-to-book / stale-price problem, and how you handle it defines the product.
Naive approach: trust the cached search price and book at it.
user selects cached itinerary at Rs 4,500
create PNR, charge Rs 4,500
Where it breaks:
- The cache is stale by design. The fare cache is refreshed on a timer; between refreshes the airline may have raised the fare or sold out the cheap booking class. Booking at the cached price means either the airline rejects the PNR (fare no longer valid) or you absorb a growing loss on every mispriced booking.
- Availability, not just price, goes stale. The Rs 4,500 fare might exist but in a booking class (RBD) with zero seats left. A price without an available seat is fiction.
- High look-to-book abuse. Repricing every itinerary shown would restore accuracy but reintroduce exactly the live-fan-out cost the cache exists to avoid, and wreck your look-to-book ratio with the airlines.
First evolution: reprice only the selected itinerary, live, before payment. Do not verify what the user is merely looking at; verify what they chose. When the user selects an itinerary, call the owning supplier once to reprice - confirm the exact current fare, taxes, rules, and that the booking class has seats. This spends exactly one live call per serious booking intent, not per search, keeping look-to-book healthy.
on select(itinerary):
live = supplier.reprice(itinerary, pax) # one authoritative call
if live.price == shown_price and live.seats_available:
proceed_to_payment(live.price)
elif live.price != shown_price:
show "price updated to Rs X" -> user re-confirms # honest, early
else: # sold out in this class
offer next-cheapest available fare / re-search
Second evolution: keep the cache honest so repricing rarely surprises. The less often reprice disagrees with search, the better the funnel. Two levers:
- Freshness tiering (from Component 1) already refreshes hot fares every few minutes, so the shown price is usually current.
- Availability-aware caching: cache not just the fare but the booking-class seat count, and treat a fare in a nearly-empty RBD as volatile - shorten its TTL or flag it. Poll availability for hot routes more aggressively than cold ones. A fare with 9 seats in its class is stable; the same fare with 1 seat is a coin flip and should be refreshed or hedged.
Third evolution: manage the discrepancy commercially, not just technically. Some staleness is unavoidable. Policy decisions bound the damage:
- Small increase (< threshold): optionally absorb via markup buffer, book silently.
- Larger change: always surface to the user before charging - never a silent overcharge.
- Track discrepancy rate per supplier/route as a health metric; a route whose reprice
often disagrees is under-refreshed -> raise its cache cadence or shorten its TTL.
The principle: search is intentionally approximate and served from cache; correctness is deferred to a single live reprice of the selected itinerary just before payment. Freshness tiering and availability-aware caching keep reprice-disagreement rare, and a clear commercial policy handles the residual so the user is never silently overcharged and the airline is never sent an invalid fare. This is the reconciliation that makes the whole cheap-approximate-search design safe.
API Design & Data Schema
The read path is cacheable and streamable; the verify/book path is a small set of live, idempotent operations.
REST / streaming endpoints
POST /api/v1/search
body: { origin:"BLR", dest:"DEL", depart_date:"2026-08-12",
return_date?, adults:1, children:0, infants:0, cabin:"ECONOMY",
filters?: { max_stops, airlines[], time_window, refundable } }
-> 200 { search_id, results:[ {itinerary_id, segments[], stops, duration_min,
price, currency, airlines[], refundable,
priced_at, source } ],
complete:false } # cached partial, more streaming
GET /api/v1/search/{search_id}/stream (SSE / WebSocket)
-> event: result { itinerary_id, ... } # fresher/live itineraries as they land
-> event: done { complete:true } # deadline reached, stream closed
GET /api/v1/search/{search_id}?sort=price&filter=nonstop # re-slice from session cache
-> { results:[ ... ] } # no backend recompute
GET /api/v1/itineraries/{itinerary_id}
-> { segments[], fare_breakdown, baggage, rules, layovers[] } # detail, from cache
POST /api/v1/itineraries/{itinerary_id}/reprice # the one live call per booking intent
body: { adults, children, infants }
Idempotency-Key: <client-generated>
-> 200 { price, currency, seats_available:true, fare_valid_until,
changed:false } # matches search
-> 200 { price, changed:true, old_price } # price moved -> user re-confirms
-> 409 { reason:"class_sold_out", alternatives:[ ... ] }
POST /api/v1/bookings # hand off to booking service
body: { itinerary_id, reprice_token, pax_details[], contact }
-> 201 { booking_id, state:"PENDING_PAYMENT", amount, pnr:null }
Search returns fast from cache with complete:false; the client opens the stream for refinements. reprice is idempotent (idempotency key + a short-lived fare_valid_until token) so retries never spawn duplicate live lookups or bookings.
Data stores - the right tool per plane
1. Fare cache - in-memory KV (Redis / a KV cluster), keyed by route-date-cabin. The workhorse of the serving plane. Holds the pre-priced top-N itineraries per combination, TTL-tiered by route heat. Not a source of truth - a fast, deliberately-approximate cache derived from the ingestion plane.
Key: fare:{origin}:{dest}:{yyyy-mm-dd}:{cabin}
Val: [ { itinerary_id, segments:[ {carrier, flight_no, dep, arr, dep_ts, arr_ts} ],
stops, duration_min, fare_amount, tax, currency, rbd, seats_hint,
rules_ref, source, priced_at } ] # top ~30, sorted by price
TTL: hot route ~2 min, warm ~20 min, cold absent (computed on demand)
2. Schedule / route graph store - columnar or graph DB, refreshed daily. The static-ish skeleton the assembly engine walks: flights, times, equipment, operating days, and precomputed viable connection hubs per O-D. Sharded by origin region.
Table: flights
carrier, flight_no, origin, dest, dep_time_local, arr_time_local,
operating_days (bitmask Mon-Sun), equipment, effective_from, effective_to
Index: (origin, dep_time), (origin, dest)
Table: min_connect_time
airport, from_carrier, to_carrier, domestic/intl, mct_minutes
Table: route_suppliers # route -> which suppliers/GDSs hold this content
origin, dest, supplier_id, priority
Index: (origin, dest) # bounds live fan-out to relevant suppliers only
3. Availability store - fast KV, per (flight, date, RBD). Booking-class seat counts, polled/subscribed per supplier. Consulted so a cached fare is not offered when its class is sold out; hotter TTL than fares because seats vanish fast.
Key: avail:{carrier}:{flight_no}:{date}:{rbd} -> seats_left (TTL seconds-minutes)
4. Fare-rules + filings store - document/columnar (bulk, slow-changing). ATPCO fare records and rule sets, refreshed on the filing cadence, consumed by the shopping engine to price itineraries. Read-mostly, batch-loaded.
Why this mix, not one database. The planes have opposite needs. Search needs microsecond, massively-parallel reads of approximate data - an in-memory KV cache is exactly right and a durable relational query per candidate would melt under fan-out. Schedules are a graph/relational problem, slow-changing, batch-loaded. Availability is small, hot, and volatile - a KV with tight TTL. Fare filings are bulk read-mostly documents. There is no strongly-consistent transactional store in search at all, because MakeMyTrip owns no inventory to keep consistent - the source of truth is the airline, reached at reprice/book time. The schema decision is: cache aggressively for the read plane, batch-load the slow reference data, and defer all correctness to the supplier’s live reprice.
Sharding. The fare cache and availability store shard naturally by route key (origin, dest) (or by origin region), co-locating a route’s data and letting hot routes get more replicas. The schedule graph shards by origin region, so a domestic search touches a couple of shards. No cross-shard transaction exists because there is no transaction - the write plane (ingestion) and read plane (search) are decoupled, and booking’s consistency lives in the external supplier and the separate booking/PNR service.
Bottlenecks & Scaling
Where it breaks first, in order, and the fix for each:
1. Supplier-call fan-out (breaks first, and hardest). Calling many live suppliers per search is too slow, too costly, and rate-limited to death. Fix: an asynchronous fare cache maintained by an ingestion/shopping engine that prices popular O-D-date combinations ahead of demand, serving >95% of searches with zero live calls; route-to-supplier mapping so any live fan-out hits only the handful of relevant suppliers; per-supplier rate-limit token buckets and look-to-book accounting.
2. Tail-latency from the slowest supplier. Synchronous “wait for all” makes p95 equal the worst supplier. Fix: cache-first serving with progressive/streamed results and a hard deadline - paint from cache in a few hundred ms, stream in live refinements, drop stragglers at the cutoff. The fast path is never gated by the slow one.
3. Supplier outages. One airline down should not blank the page. Fix: circuit breakers + timeouts per adapter; the search proceeds with cache and the suppliers that answered. Partial rendering and an honest “updating” note instead of an error. No single supplier is a SPOF for search.
4. Stale prices at book time (the look-to-book / correctness bottleneck). The cache disagrees with the airline. Fix: reprice only the selected itinerary live before payment - one authoritative call per booking intent, not per search; freshness tiering and availability-aware caching so reprice rarely disagrees; a commercial discrepancy policy (absorb tiny moves, always surface larger ones) so the user is never silently overcharged and the airline never gets an invalid fare.
5. Hot routes / hot keys. BLR-DEL, DEL-BOM concentrate a huge share of traffic on a few cache keys. Fix: replicate hot cache keys across the KV cluster and to the edge/CDN as short-TTL result snapshots; result cache shared across users searching the same (route, date, cabin) in the same window; refresh hot keys most aggressively. Heat is handled by replication and shorter TTLs, not by hitting suppliers more.
6. Redundant compute across identical searches. Thousands of users searching the same route. Fix: layered caching - edge snapshot, shared result cache, per-session cache for filter/sort/paginate. Repeated and near-identical work collapses to a single computed answer reused many times.
7. Combinatorial blow-up in itinerary assembly. Unbounded connection search explodes. Fix: bound the graph search (max stops 1-2, MCT, max layover, no backtracking), precompute connections for hot routes into the cache, and price candidates from the cache (not a live call each). Cold routes run the bounded search on demand and cache the result briefly.
8. Availability staleness (sold-out cheap classes). A cached fare’s booking class has no seats. Fix: a separate availability store with tighter TTL than fares; flag fares in nearly-empty RBDs as volatile; the reprice step is the final seat check, offering the next-cheapest available fare on a sell-out.
9. Ingestion pipeline load and skew. Pricing every combination is infeasible; effort must follow demand. Fix: tier by route heat - price hot routes every few minutes, warm every tens of minutes, cold on demand; drive priorities from real search-traffic telemetry so the shopping engine spends compute where users actually look. Silently log the long-tail routes served purely on-demand so coverage gaps are visible, not hidden.
10. Fare cache size and cold-start. A cold cache means every search is a live fetch. Fix: warm the cache from search telemetry (pre-fetch trending routes and dates), keep only hot+warm cells resident (~500 GB per the BoE), and compute-and-cache cold cells on first request with a short TTL. Sharding by route key keeps hot cells independently scalable.
11. Streaming/connection fan-in at peak. Millions of open SSE/WebSocket streams during a sale.
Fix: stateless streaming gateways behind the LB with the search state in the shared cache keyed by search_id, horizontally scaled; a short stream lifetime (results close at the deadline) so connections are not long-lived.
12. Single points of failure. Fix: the KV cache clusters (fare, availability, result) run replicated with failover; the ingestion pipeline is a set of stateless workers off the request path, so its lag degrades freshness, never availability; schedule/filing stores are batch-loaded replicas. Because search owns no source-of-truth state and defers correctness to the supplier at reprice, any single component failure degrades freshness or coverage - it never corrupts a booking. The system degrades, never lies: worst case it shows less or slightly staler data, and the live reprice remains the guard before any charge.
Wrap-Up
The trade-offs that define this design:
- A warm cache over live fan-out. Serving searches from an asynchronously-maintained fare cache instead of calling 50 suppliers live is the single decision that makes MakeMyTrip fast, cheap, and viable with the airlines. Live calls are a narrow exception (cold routes, and the one reprice per booking), never the rule.
- Approximate search, correct booking. Search is openly eventually-consistent and served from cache; correctness is deferred to a single live reprice of the selected itinerary just before payment. We spend authoritative work per booking intent, not per look, keeping the look-to-book ratio healthy and the pages instant.
- Assemble itineraries with a bounded graph search, priced from cache. Connections across different airlines are first-class products built by a time-expanded schedule search bounded by MCT/stops/duration and priced from the fare cache, with hot routes precomputed - so cross-airline connections appear without a combinatorial blow-up or a live call per candidate.
- Progressive results with hard deadlines and circuit breakers. Paint from cache in a few hundred ms, stream in fresher data, drop stragglers at the cutoff, and skip failing suppliers - so the fast path is never gated by the slowest airline and no single outage blanks the page.
- Match every store to its plane. In-memory KV caches for the read plane, a graph store for schedules, a tight-TTL availability store, bulk filing storage - and deliberately no transactional store in search, because the source of truth is the airline, reached at reprice time.
One-line summary: a MakeMyTrip-style flight search where an asynchronous ingestion pipeline prices popular routes into a heat-tiered fare cache, a bounded schedule-graph engine assembles direct and cross-airline connecting itineraries, cache-first progressive serving with hard deadlines and circuit breakers returns ranked results in under two seconds without touching 50 live suppliers, and correctness is deferred to a single idempotent live reprice of the selected itinerary just before payment - so search stays fast and cheap while the price the user pays stays honest.
Comments