← All projects Low-Level Design · Interview Deep Dive

MakeMyTrip — LLD

Complete interview-grade design: requirements, entities, patterns, concurrency, cross-questions, and the mental model you need to arrive at this design on your own.

Start here

How to build the intuition

Before any class diagram, anchor yourself on 4 mental moves. Every good LLD answer comes from these — not memorisation.

1. Find the "product" abstraction

MakeMyTrip sells flights, hotels, buses, trains, cabs, holiday packages. They look different but behave similarly: search → view → hold inventory → pay → confirm → cancel/refund. That similarity is your abstraction — a Bookable or TravelProduct. Every LLD question has one hidden abstraction; find it first, the rest of the design falls out.

2. Separate "what changes" from "what doesn't"

The booking lifecycle (search → hold → pay → confirm) rarely changes. What changes: how inventory is fetched (GDS vs own DB), how price is computed, how payment is charged. Stable stuff → templates / skeletons. Variable stuff → strategies / plugins. This single move produces 80% of the design patterns you'll use.

3. Draw the money & inventory flows, not the screens

Interviewers don't care about UI. They care: when does money move, when does inventory lock, what happens if one fails. Draw two parallel timelines — inventory state vs payment state — and design around their intersections (holds, compensations, idempotency).

4. Every "what if X fails" → a design decision

Treat failures as first-class. "Payment timed out after seat was held" is not an edge case; it's the normal case at scale. The moment you accept that, you start reaching for state machines, sagas, idempotency keys, and compensation — without being told to.

Interview tip: Say these 4 moves aloud at the start. It signals maturity and buys you time to think. Then say: "Let me clarify scope first" — and ask the questions below.

Step 1

Clarifying questions to ask

Never start coding. Ask these, write the answers on the board — they scope your whole design.

📦 Scope

  • Which verticals? (flights only? flight + hotel? packages?)
  • B2C only or also B2B/agent portal?
  • One-way, round-trip, multi-city?
  • Single traveller or group bookings?

🗂️ Inventory

  • Own inventory or aggregator (GDS: Amadeus/Sabre)?
  • Real-time or cached search results?
  • How long do we hold a seat before payment?

💳 Payments

  • Which methods? (card, UPI, wallet, EMI, pay-later)
  • Split payments? (wallet + card, coupon + UPI)
  • Partial refunds, cancellation fees?

⚖️ Non-functional

  • QPS for search vs booking?
  • Consistency: is "no double booking" strict?
  • SLA for search latency? booking latency?
Step 2

Requirements

Assume a typical interview scope: flight + hotel booking, card/UPI/wallet payments, single traveller or group.

Functional

  • Search flights / hotels with filters (price, stops, rating, time)
  • Hold inventory for N minutes during checkout
  • Accept payment (card, UPI, wallet, coupon, EMI — stackable)
  • Confirm booking, issue e-ticket / voucher
  • Cancellation with partial/full refund per fare rules
  • User wallet, loyalty points, coupons
  • Notifications: email, SMS, push, WhatsApp
  • Price alerts for saved searches

Non-functional

  • No double booking — strict single-seat invariant
  • Idempotent booking & payment APIs
  • Search p95 < 800ms, booking p95 < 2s
  • Graceful degradation if GDS is slow (show cached fare, warn user)
  • Audit trail for every money / inventory mutation
  • Extensible to new products (train, bus) without changing core
Step 3

Core entities

Keep nouns small and specific. These are the ~15 classes you'll end up drawing.

EntityResponsibilityKey fields / relations
UserAuth, profile, travellers, wallet, loyalty tierid, email, phone, walletRef, tier
TravelProduct (abstract)Common contract: id, price, availability, fareRulesFlight / Hotel / Bus extend this
FlightRepresents a flight segment/legairline, from, to, dep/arr, seatMap, fareClasses
HotelProperty with room typeslocation, amenities, roomInventory[]
SearchCriteriaImmutable query objectorigin, destination, dates, pax, class, filters
SearchResultListing of products + faceted filtersproducts[], appliedFilters
CartItems + add-ons + price breakdownitems[], addons[], priceBreakup
PriceBreakupBase + tax + fees + discount + payablelineItems[], total, payable
Hold / InventoryLockTime-bound reservationproductId, pax, expiryAt, holdId
BookingAggregate root for a confirmed trippnr/voucher, travellers[], state, items[], payments[]
PaymentOne attempt/capturemethod, amount, status, gatewayRef, idempotencyKey
Coupon / Wallet / LoyaltyPointsValue adjusters applied before paymentrules engine decides stackability
CancellationPolicy / FareRulePure function: returns refundable amountslabs (hrs before departure → % refund)
RefundReverse money flow, tracks statebookingId, amount, toMethod, status
NotificationMulti-channel messagechannel, template, recipient, status
Step 4

Design patterns — what and why

Each pattern maps to a specific variability axis. Saying "we'll use Strategy" isn't enough — explain what varies.

Strategy — search, filter, pricing, payment, refund

Varies: how an operation is done. Search flights via GDS vs own DB; price with dynamic-pricing vs static; pay via Stripe vs PayU vs UPI; apply coupon as %-off vs flat-off vs BOGO.

Define PricingStrategy, SearchStrategy, PaymentStrategy, CouponStrategy. Inject via constructor. New algorithm = new class, zero change to core.

Factory — product & provider creation

Varies: which concrete object to build. ProductFactory.create(type) returns Flight, Hotel, etc. PaymentGatewayFactory returns the right gateway client. Keeps if-else ladders out of business logic.

Builder — complex booking & cart

Problem: a booking has travellers, add-ons (meal/seat/insurance), coupons, payment splits. Constructor with 10 args is unreadable. Booking.builder().traveller(…).addon(…).coupon(…).split(…).build() is clean and enforces invariants in build().

Decorator — add-ons on top of base fare

Varies: price composition. FarePrice → + SeatFee → + MealFee → + InsuranceFee → + ConvenienceFee. Each decorator wraps the previous, overriding getAmount(). Add-ons become pluggable without editing base fare code.

Chain of Responsibility — checkout pipeline

Booking request flows through: ValidateRequest → CheckInventory → ApplyCoupon → ReservePayment → ConfirmWithSupplier → GenerateTicket → Notify. Each handler can pass, fail (with compensation), or short-circuit. Makes cross-cutting steps (fraud check, rate limit) trivially insertable.

State — booking lifecycle

Booking states: INITIATED → HOLD → PAYMENT_PENDING → PAYMENT_FAILED / CONFIRMED → CANCELLED / REFUND_PENDING / REFUNDED. Encode transitions in a State pattern (or table) so invalid moves (e.g. cancel an already-refunded) fail fast.

Observer — notifications & price alerts

On BookingConfirmed, observers fire: email, SMS, WhatsApp, loyalty-point-credit, CRM sync. On PriceDrop, saved-search subscribers get push notifications. Decouples event producers from handlers.

Template Method — booking skeleton

The algorithm outline (hold → pay → confirm → notify) is identical for flight, hotel, bus. Define AbstractBookingService.book() as a template that calls abstract hooks (holdInventory(), confirmWithSupplier()) — concrete services fill the blanks.

Singleton — caches & config

One in-memory fare cache, one GDS client pool, one config store. Spring-style DI already gives you this — just be explicit that you want one instance per JVM.

Facade — booking API surface

BookingFacade.book(request) hides the chain, state machine, saga, notifications. Callers (mobile, web, partner) see one clean method.

Saga / Compensating transaction (pattern, not GoF)

Money and inventory live in separate systems. A saga ensures: if payment captured but supplier confirmation failed → auto-refund + release hold. Each step has a compensation. Critical to mention in any booking LLD.

Step 5

Class-level design (pseudo-Java)

Skeleton the interviewer actually expects to see. Focus on interfaces & signatures — implementations are one-liners.

core abstractions
interface TravelProduct {
    String id();
    ProductType type();          // FLIGHT, HOTEL, BUS…
    Money basePrice();
    Availability availability(SearchCriteria c);
    FareRule fareRule();
}

interface InventoryProvider<P extends TravelProduct> {
    List<P> search(SearchCriteria c);
    Hold           hold(String productId, int pax, Duration ttl);
    void           release(String holdId);
    SupplierRef    confirm(String holdId);   // returns PNR/voucher
}

interface PricingStrategy   { PriceBreakup price(Cart c, User u); }
interface PaymentStrategy   { PaymentResult charge(PaymentRequest r); }
interface CouponStrategy    { Money discount(Cart c, Coupon k); }
interface RefundStrategy    { Refund refund(Booking b, Money amt); }
booking aggregate & state
class Booking {
    final String id;
    final User user;
    final List<BookingItem> items;         // flight + hotel etc.
    final List<Payment>     payments;      // split payments
    private BookingState state;           // State pattern

    void transition(BookingEvent e) {
        state = state.on(e, this);       // enforces legal transitions
    }
}

interface BookingState { BookingState on(BookingEvent e, Booking b); }
// Concrete: InitiatedState, HeldState, PaidState, ConfirmedState,
//            CancelledState, RefundedState… each implements legal moves only
checkout chain
abstract class CheckoutHandler {
    protected CheckoutHandler next;
    public final BookingContext handle(BookingContext ctx) {
        process(ctx);
        return next == null ? ctx : next.handle(ctx);
    }
    protected abstract void process(BookingContext ctx);
}

// Concrete handlers — each focused, each compensatable
ValidateRequest  → CheckFraud → HoldInventory → ApplyCoupon
                 → ReservePayment → ConfirmWithSupplier
                 → GenerateTicket → PublishEvents → Notify
pricing with decorators
interface Priced { Money amount(); }

class BaseFare implements Priced { … }

abstract class FareAddon implements Priced {
    protected final Priced inner;
    public Money amount() { return inner.amount().plus(feeAmount()); }
    protected abstract Money feeAmount();
}
// SeatSelection, MealUpgrade, InsurancePlan, ConvenienceFee…
facade
class BookingFacade {
    BookingResponse book(BookingRequest r) {
        var ctx = new BookingContext(r);
        checkoutChain.handle(ctx);           // may throw, triggers saga
        return BookingResponse.from(ctx.booking);
    }
}
Step 6

Booking state machine

Draw this on the whiteboard. Every transition has a trigger event and, ideally, a compensation.

    [INITIATED]
         │ selectProduct
         ▼
    [CART_READY] ── holdFailed ──► [ABANDONED]
         │ holdOk
         ▼
    [HELD] ─── holdExpired ───► [EXPIRED] (auto-release)
         │ paymentSubmitted
         ▼
    [PAYMENT_PENDING] ── paymentFailed ──► [PAYMENT_FAILED] ──► release hold
         │ paymentCaptured
         ▼
    [PAID] ── supplierConfirmFailed ──► [SUPPLIER_FAILED] ──► refund (saga)
         │ supplierConfirmed
         ▼
    [CONFIRMED] ── userCancel ──► [CANCELLATION_REQUESTED]
                                      │ applyFareRules
                                      ▼
                                 [REFUND_PENDING] ──► [REFUNDED] / [PARTIAL_REFUNDED]

Invariant to verbalise: money only moves with inventory, and inventory only commits with money. Any divergence → saga compensates.

Step 7

Concurrency & atomicity

This is where LLD interviews separate mid from senior candidates. Be specific.

🔒 No double booking

  • Optimistic lock on seat row (version column). Update with WHERE version = ? — if 0 rows updated, retry.
  • Pessimistic lock (SELECT … FOR UPDATE) when a single row represents hot inventory (flash sale).
  • Redis atomic DECR / Lua script for inventory counters in high-QPS search→hold.
  • Unique DB constraint on (flightId, seatNumber, date) as final safety net.

🪪 Idempotency

  • Client sends Idempotency-Key header on every POST to /book and /pay.
  • Server stores (key → response) in Redis with TTL; replays return cached response.
  • Same key on payment provider side (payment.idempotencyKey) prevents double-charge on retry.

⏱️ Hold expiry

  • Hold row has expiresAt. A background job sweeps expired holds and releases inventory.
  • Or: Redis key with TTL → on expiry event, publish to Kafka → release.
  • On payment arrival, check expiresAt > now under lock before capturing.

🔁 Retries & timeouts

  • Every external call (GDS, payment) has timeout + bounded retry + circuit breaker.
  • Never retry a non-idempotent call without an idempotency key.
  • On unknown state (timeout), mark PENDING and reconcile async — never assume failure.
Step 8

Payment, split, coupon, refund

Split payment math

Payable = Base + Tax + Addons − Discount. A booking can be paid via multiple Payment rows (wallet ₹500 + UPI ₹2300). Invariant: sum(payments.amount) == booking.payable. Each Payment has its own status and gateway reference; the booking only transitions to PAID when all payments are captured.

Coupon / wallet / loyalty stacking

Apply in a defined order to avoid gaming: coupon → loyalty → wallet → gateway. A DiscountEngine (chain of CouponStrategy, LoyaltyStrategy, WalletStrategy) produces a final PriceBreakup. Stackability rules live in config, not code — new coupon type = new row.

Refund flow

Refund original payment method first (RBI requirement). If a payment was split across wallet + card, refund each proportionally. Refund state: REQUESTED → INITIATED → SUCCESS / FAILED; failed refunds retry with jitter. Full trace via refundId → paymentId → bookingId.

Step 9

Cross-questions & model answers

These are the follow-ups I've personally been asked or asked as interviewer. Answer briefly, lead with the invariant.

Q1. Payment succeeded but supplier confirmation failed. What now?
Saga compensation. Move booking to SUPPLIER_FAILED, trigger refund to original method, release hold, notify user. Never auto-retry supplier blindly — do 1–2 idempotent retries, then compensate. Log everything for reconciliation.
Q2. Two users try to book the last seat at the same instant.
Only one wins. Use optimistic locking on seat row (UPDATE … WHERE version = ?) or Redis Lua atomic decrement. Loser gets a "sold out, please pick another" response before payment page — never after money moves.
Q3. User refreshes / double-clicks "Pay". How do you prevent double charge?
Client generates an Idempotency-Key when entering the pay screen. Server de-dupes by that key in Redis (24h TTL) and forwards the same key to the gateway. Any retry returns the first attempt's result.
Q4. How do you support a new product type — say, cruise — with minimal change?
Implement TravelProduct, plug a CruiseInventoryProvider, extend AbstractBookingService. The checkout chain, state machine, payment, notifications reuse as-is. This is why we chose Template Method + Strategy up front.
Q5. How does dynamic pricing fit in?
PricingStrategy plugs in — a DynamicPricingStrategy calls an ML service with context (occupancy, time-to-departure, user segment). Cache the output with short TTL keyed on (productId, segment); fall back to base fare on ML timeout.
Q6. Cancellation — who computes the refundable amount?
CancellationPolicy.compute(booking, now): pure function, slab-based (e.g. ">48h → 90%", "24–48h → 50%", "<24h → 0%"). Airlines send these slabs via fare rules; we store them with the booking so refund math is reproducible even if rules change later.
Q7. How do you audit "who changed what" across booking, payment, inventory?
Every state mutation emits an event to an append-only event log (Kafka + persistence). Each event has entityId, actor, before, after, reason. Reconciliation jobs replay to detect divergence.
Q8. What if the GDS is down during search?
Serve cached results with a "prices may change" flag. On booking, always re-validate with GDS under a fresh hold; if re-price differs by > ε, reconfirm with user. Never book against stale price.
Q9. Group booking of 6 seats — all-or-nothing?
Atomic hold: acquire all 6 seats in a single transaction or Redis Lua. If fewer available, hold none and return. On payment failure, release all 6. Never partial-confirm unless product explicitly allows it.
Q10. How do you prevent coupon abuse / replay?
Coupon has maxUsesPerUser, globalMaxUses, validFrom/To. Apply decrement atomically (Redis INCR against limit). Bind to user & booking on success. Abuse detection: count applications per device/IP over window.
Q11. Notifications — email failed, do we retry forever?
No. Dead-letter queue after N attempts with exponential backoff. Observers are non-blocking; booking confirmation never depends on notification success. Surface "resend ticket" UI for recoverable cases.
Q12. How do you test this without live payments/GDS?
Abstract every external boundary behind an interface (PaymentGateway, InventoryProvider). Use in-memory fakes for unit tests, contract tests against sandbox, end-to-end on staging with provider sandboxes. Property tests for state machine transitions.
Q13. Timezones — flight departs 23:55 Delhi, arrives 04:30 London next day. Where do you store times?
Store everything in UTC + origin IANA zone (Asia/Kolkata) + destination zone. Display in local zone. Fare rules (e.g. ">24h before departure") are computed against departure local time, not UTC.
Q14. A user has both flight + hotel in one booking. Flight confirmed, hotel supplier fails.
This is a multi-leg saga. Options: (a) ask user — keep flight, cancel+refund hotel, or cancel both; (b) policy-driven auto-choice based on cancellation cost. Either way, the booking goes to PARTIAL_CONFIRMED and action is explicit, not silent.
Q15. Scaling search to millions of QPS — is this still LLD?
No — redirect to HLD (Elastic/OpenSearch cluster, fare cache, read-through). But mention the LLD hook: SearchStrategy has a CachedSearchStrategy decorator; the domain classes don't change.
Before you wrap up

Interview checklist

Run through this mentally in the last 5 minutes — catching a miss here adds one full point to the final rating.

Things to explicitly call out

  • Named the hidden abstraction (TravelProduct) early
  • Separated lifecycle (Template) from behaviour (Strategy)
  • Drew the state machine with terminal states on both success and failure
  • Mentioned idempotency keys on every mutating API
  • Addressed concurrency (optimistic vs pessimistic vs Redis atomic — and which where)
  • Acknowledged saga / compensating transactions for cross-system consistency
  • Called out audit trail via event log
  • Showed extensibility: "to add cruise, implement X, everything else stays"
  • Handled timezones, split payments, partial refunds, group bookings
  • Connected back to the user (cancellation UX, failure messaging)

Final line to say: "I've kept the domain model small and pushed variability into strategies. That way, adding a new travel product or payment method is additive — no change to the core booking lifecycle. Happy to deep-dive into any piece."