~learn.

Side quest 17Food delivery

Swiggy Food Delivery.

A Hot Meal, a Kitchen You Don't Control, and a Rider Who Arrives Just as the Food Does

HARD40 MIN READ6 PHASES · 45 MIN INTERVIEW+120 XP

Mission brief

Design on-demand food delivery

Quest log0/7 stages · 0/7 checkpoints · 0/7 quick checks
monthly food-delivery customers (Apr-Jun 2026)
19.2 million
restaurants with an order each month
2.66 lakh
delivery partners with a delivery each month, platform-wide
6.48 lakh
biryanis a minute at the New Year's Eve 2025 peak
1,336

Company names and logos are trademarks of their owners, shown only to identify each company. This course isn't affiliated with or endorsed by any of them.

Optional · timed run · 6 phases · 45 min

Want it harder? Hide the model answer and work each phase in a scratchpad against its time budget, then reveal and score yourself. Or simply read on: the checkpoints below work either way.

Intro · 3 min

How Do You Get a Biryani From a Kitchen You Don't Run to a Door Across the City While It's Still Hot?

It is 8:40 PM on a Saturday. A customer in Koramangala orders a biryani and two kebabs from a restaurant three kilometres away. The app says 34 minutes.

In the restaurant's kitchen, a tablet chimes. The cook has eleven orders on the rail and the tandoor is full. Somewhere in the system, a decision is waiting to be made: which rider should pick this order up, and when should they start riding? Too early, and the rider stands at the counter for fifteen minutes doing nothing. Too late, and the biryani sits on the pass going cold.

The restaurant isn't ours. The kitchen isn't ours. The roads aren't ours. The only thing the platform controls is information, and it has to turn that into a time the customer can trust.

Swiggy, from published numbers
  • ₹9,490 crore — food delivery gross order value in April-June 2026, up 17.4% year on year (Swiggy Q1 FY27 shareholders' letter)
  • 19.2 million — average monthly transacting food-delivery users that quarter (27.5 million across the whole platform)
  • 2,65,800 — average monthly transacting restaurant partners
  • 6,48,000 — average monthly transacting delivery partners, platform-wide
  • 308 million — orders across Swiggy's consumer businesses that quarter (food, Instamart and dining out), 114.5 million of them on Instamart
  • 93 million — biryanis ordered from 1 January to 30 November 2025, about 194 a minute; on New Year's Eve the rate peaked near 1,336 a minute just before 8 PM
  • One minute — the service-level target for scoring every order against every eligible rider in a city (Akka's Swiggy customer story)

For context:
Swiggy's letter for that quarter blamed part of its slower growth on restaurant-driven cancellations during LPG supply disruptions: the kitchen, not the app, can be the bottleneck
India's Code on Social Security, in force since 21 November 2025, requires aggregators to pay 1-2% of turnover (capped at 5% of payouts to gig workers) into a welfare fund
Everything in this design that is not in the list above is an illustrative design target, not Swiggy's internal architecture

This is the system we'll design today.

ExplainerThe whole quest: Swiggy

Swiggy serves 19.2 million food customers a month from 2.66 lakh restaurants with 6.48 lakh riders; food ready in 15 minutes and a rider 6 minutes away means dispatching at about 7.5 minutes, not at once. The read plane lists restaurants that can deliver now, an order saga gives every kitchen step a timeout and a refund, assignment matches due orders to riders in 20-second city rounds, and riders ping every 4 s. The deep dives cover kitchens as partners, joint assignment and batching, the ETA as a calibrated distribution and location ingest, then rain, kitchens that can't cook and New Year's Eve in the rain.

The Interview Begins#

You're interviewing at a company building a food-delivery marketplace for Indian cities. The engineering director draws three circles on the whiteboard: customers, restaurants, riders.

Interviewer

"Three sides, and we only own the software between them. Design the system that takes a food order from tap to door: the restaurant, the rider, the tracking, the arrival time, and what happens when any of it goes wrong."

Design on-demand food delivery

A customer browses nearby restaurants, orders, and pays. The restaurant accepts and cooks. A rider is assigned, picks up the food and delivers it, while the customer watches the rider on a map. The platform runs in hundreds of cities with a sharp dinner peak and bigger spikes on match nights, in the rain and on New Year's Eve.

Requirements
  • Show only restaurants that can deliver to this address now, with an honest arrival time
  • Get the order accepted or rejected by the restaurant quickly; never leave it hanging
  • Assign a rider who reaches the restaurant about when the food is ready
  • Batch two orders on one trip only when both still arrive on time
  • Live tracking for the customer; navigation and earnings for the rider
  • Charge once; refund automatically when an order fails
  • Never put the rider under time pressure: the estimate absorbs a slow night
Interviewer

"Most candidates copy ride-hailing and match the nearest rider the moment an order comes in. Tell me why that's wrong for food."

Phase 1 · Stage 1 of 7 · 5 min budget · 3 min read

Phase 1: Requirements Clarification (5 minutes)#

You

"The difference from a ride-hailing app is the kitchen. A car is ready the moment it arrives; food isn't. I want to understand how much we know about the kitchen."

Your Questions#

Transcript
You

"Does the restaurant have to accept each order, or is it automatic?"

Interviewer

"Assume most restaurants confirm on a tablet or their own system, and some auto-accept. A restaurant can reject an order, mark items out of stock, or go offline. In April-June 2026, Swiggy's own letter to shareholders called out restaurant-driven cancellations during LPG supply disruptions. So the kitchen can fail in ways the app doesn't see."

You

"Do we know how long a dish takes to cook?"

Interviewer

"Not exactly. The restaurant may tell you an estimate, you have history for that restaurant, that dish and that time of day, and the kitchen's current load. You never see inside it."

You

"Do riders work for a store, like quick commerce, or float around the city?"

Interviewer

"They float. A rider goes online in an area, gets offers, and after each delivery is somewhere new. Swiggy had about 6.48 lakh delivery partners making at least one delivery a month in April-June 2026, across all its businesses."

You

"How big is the order volume?"

Interviewer

"Swiggy's consumer businesses did 308 million orders that quarter, 114.5 million of them on Instamart, so food plus dining out was a little over 2 million a day. Size it yourself from that."

You

"And the peaks?"

Interviewer

"Every evening around 8 to 10 PM. Then the spikes: rain, which raises demand and slows riders at the same time; big cricket matches; and New Year's Eve, when biryani alone peaked near 1,336 orders a minute just before 8 PM, against a 2025 average of about 194 a minute."

You

"How is the arrival time judged?"

Interviewer

"By whether you kept it. Too short and customers are angry; too long and they don't order. Tell me how you'd pick the number."

Functional Requirements#

Functional requirements
1. Discovery
  • From the delivery address, list restaurants that can deliver now, with an arrival time and a fee
  • Menus with live item availability; search across dishes and restaurants
2. Ordering
  • Cart, coupons, taxes and delivery fee; pay by UPI, card, wallet or cash on delivery
  • Exactly one order per checkout attempt
3. Restaurant side
  • Order appears on the restaurant's tablet or system; accept with a prep time, or reject
  • Mark items out of stock, pause the restaurant, mark food ready
4. Riders
  • Go online in an area; receive order offers; accept
  • Navigate to the restaurant, pick up, navigate to the customer, deliver with proof
  • Batching: occasionally two orders on one trip
5. Customer during delivery
  • Live status and rider location; a revised arrival time when things change
  • Contact the rider, report a problem, get refunds for missing items or failed orders

Non-Functional Requirements#

Non-functional requirements
The promise
  • Arrival estimate kept (delivered before the shown time plus a small buffer) for ≥ 90% of orders, per city zone, per hour (design target)
  • Median order-to-door around 30 minutes for a ~3.5 km order (design target)
Latency
  • Restaurant list and arrival times for an address: < 300 ms p99
  • Order visible on the restaurant's tablet: < 5 s after payment
  • Rider assigned for every order inside its planned window; a city's assignment round finishes in < 1 minute
  • Rider location on the customer's map: < 10 s behind the rider
Correctness
  • Every captured rupee ends in a delivered order or a refund, automatically
  • An order is never assigned to two riders; a rider never carries an order they weren't offered
Availability
  • Browsing keeps working if ordering is degraded; ordering keeps working if tracking is degraded
  • A problem in one city never spreads to another
Fairness and safety
  • No countdowns on the rider's screen; rain stretches estimates, not speeds
  • Offers explain the trip (distance, pay) before the rider accepts

Phase 2 · Stage 2 of 7 · 5 min budget · 4 min read

Phase 2: Back of the Envelope Estimation (5 minutes)#

You

"The interesting numbers here are minutes and riders, not requests. Let me do the minutes first."

Your turn · checkpoint 1not answered

Before the numbers: when should the rider start riding?

A restaurant accepts an order and says the food will be ready in 15 minutes. The nearest free rider is 1.5 km away, about 6 minutes of riding. If you assign and dispatch that rider immediately, how long do they wait at the counter? And when should the assignment really happen?

Think it through, or jot it down. Then compare.

One strong answer

  • Assign now: the rider arrives in 6 minutes and waits about 9 minutes at the counter. Over a dinner peak, that idle time adds up to thousands of rider-hours a night across a city, riders who could have carried other orders.
  • Assign too late: if the rider arrives after the food is ready, the food waits on the pass and the arrival time slips by the same amount.
  • Just in time: aim for the rider to arrive a minute or two before the predicted ready time, so dispatch starts about 15 − 6 − 1.5 ≈ 7.5 minutes after acceptance, and keep re-checking the prediction until then. The rider can deliver another order in those minutes.

So food dispatch is a timing problem, not a nearest-rider problem. It also means the prep-time prediction drives the whole fleet: get it wrong by five minutes and either food or riders wait.

The Order-to-Door Budget#

WorksheetA ~3.5 km dinner order (design budget, median)
Step Budget Running total
Customer pays → order on the restaurant's tablet 15 s 0:15
Restaurant accepts and states a prep time 60 s 1:15
Kitchen prepares and packs (varies most) 15 min 16:15
Rider arrives just before ready, handover 2 min 18:15
Ride, ~3.5 km at ~18 km/h 12 min 30:15
Park, find the door, hand over 1.5 min 31:45

Note: the rider's first ride to the restaurant (here ~6 minutes) is hidden inside the 15 minutes of cooking. That is what just-in-time assignment buys.

Traffic Estimation#

WorksheetOrders, riders and pings (design target, rounded up from public figures)
  • Public baseline: 308 million consumer orders in a 91-day quarter, 114.5 million on Instamart, so food and dining out together = 193.5 million ÷ 91 = ~2.1 million a day
  • Design target: 2.5 million food orders a day
  • Average minute: 2.5 million ÷ 1,440 = ~1,740 orders a minute
  • Busiest dinner hour, assumed to carry 12% of the day: 300,000 orders = ~5,000 a minute, ~83 a second
  • New Year's Eve, using the biryani peak as a guide (1,336 a minute against an average of 194, about 7×): ~12,000 a minute, ~200 a second
  • Riders at the dinner peak:
    • One trip: ~6 min to the restaurant + ~2 min waiting + ~12 min riding + ~2 min at the door + ~5 min back towards busy areas = ~27 minutes, ~2.2 trips an hour
    • With ~15% of trips carrying two orders: 2.2 × 1.15 ≈ 2.5 orders a rider an hour
    • Riders online: 300,000 ÷ 2.5 = ~120,000
  • Location pings, one every 4 s from each rider online: 120,000 ÷ 4 = ~30,000 a second
  • Read traffic, assuming ~60 requests per order (restaurant lists, menus, search, cart, tracking refreshes, and the sessions that never order): 83 × 60 = ~5,000 a second at the dinner peak, ~12,000 on New Year's Eve

Conclusion: order writes are a few hundred a second at most. The heavy streams are rider locations and reads; the hard computation is assignment.

Assignment Estimation#

WorksheetOne city's assignment round
  • Largest city's share of orders, assumed: 15% → 0.15 × 83 = ~12.5 new orders a second at the dinner peak
  • Assignment runs in rounds every 20 s: ~250 orders a round become due for a rider
  • Candidate riders per order (free or finishing soon, within ~3 km of the restaurant): ~40
  • Pairs to score: 250 × 40 = 10,000 per round
  • At 5,000+ predictions a second on one model (the rate in Akka's Swiggy story), one round's scoring takes ~2 s on one model instance; a few instances keep it well inside the one-minute target
  • Matching 250 orders to riders from a 250 × 40 score table: milliseconds

Conclusion: assignment fits easily if it is batched per city and per round. Doing it one order at a time, greedily, is not slower; it is worse, because it gives the best rider to whichever order happened to arrive first.

Storage Estimation#

WorksheetSmall, except for the location trail
  • Orders: 2.5 million a day × ~3 KB (lines, prices, taxes, state changes, rider and payment references) = ~7.5 GB a day, ~2.7 TB a year
  • Restaurants and menus: 2.66 lakh restaurants × ~150 items × ~1 KB = ~40 GB, served from caches
  • Latest rider positions: 120,000 × ~64 bytes = ~8 MB, in memory
  • Location trail, averaging ~15,000 pings a second over the day: 15,000 × 86,400 = 1.3 billion pings × ~40 bytes = ~52 GB a day, kept hot for a few days, then summarised into trip paths

Key Metrics Summary#

Metric Value
Food orders a day (design target) 2.5 million
Orders at the dinner peak / New Year's Eve ~83/s / ~200/s
Riders online at the dinner peak ~120,000
Location pings ~30,000/s
Pairs scored per city round ~10,000 every 20 s
Median order-to-door, ~3.5 km ~32 min
Latest rider positions in memory ~8 MB
Quick check3 questions · +5 XP each

Requirements and estimation

  1. Q1/3 Food is ready in 15 minutes and the nearest free rider is 6 minutes away. If you dispatch that rider immediately, how long do they wait at the counter?

    Answer & explanation

    Answer: B

    15 − 6 = 9 minutes standing at the counter. Multiply by every order at dinner and the fleet loses thousands of rider-hours; dispatching just in time recovers them.

  2. Q2/3 A trip takes about 27 minutes and 15% of trips carry two orders. Roughly how many orders does a rider deliver an hour?

    Answer & explanation

    Answer: D

    60 ÷ 27 ≈ 2.2 trips an hour, × 1.15 orders a trip ≈ 2.5. That's what turns 300,000 orders at the dinner peak into ~120,000 riders online.

  3. Q3/3 Which number in the estimate shows where the real computation is?

    Answer & explanation

    Answer: C

    Order writes and storage are modest. The work is deciding who carries what: scoring every due order against every nearby rider, then matching them, round after round.

Phase 3 · Stage 3 of 7 · 10 min budget · 3 min read

Phase 3: High-Level Design (10 minutes)#

Your turn · checkpoint 2not answered

Sketch it first: one order, from tap to door

Trace one order from "Pay" to "Delivered". Which parts are a marketplace (three parties, any of whom can say no), which are prediction, and which must be exactly right?

Think it through, or jot it down. Then compare.

One strong answer

  • Read plane: serviceability (which restaurants can deliver to this address now, and in how long), menus and search, from caches a few seconds old.
  • Exact: checkout creates one order with an idempotency key, takes the payment, and drives an order saga. Every refund and cancellation goes through it.
  • Marketplace: the order goes to the restaurant, which accepts (with a prep time) or rejects within a timeout. Riders receive offers and accept or decline.
  • Prediction: prep time, travel times and the delivery-time distribution, which together decide when to dispatch and what arrival time to show.
  • Assignment: a per-city service that, in rounds, matches orders whose dispatch time has come to riders, batching when both promises hold.
  • Tracking: rider pings into a location service; customers' maps get positions and a refreshed estimate.
You

"The design follows the three sides. Orders are few and must be exact, so they live in a small, strongly consistent order service with a saga. Restaurants and riders are partners who can say no, so every interaction with them has a timeout and a fallback. And the clever part is the planning: prediction plus assignment, per city, in rounds."

System Architecture#

DiagramFood-delivery platform architecture
%% title: Food-delivery platform architecture
flowchart TD
  subgraph L1["Layer 1 · Three apps"]
    direction LR
    CA@{ icon: "sd:phone", label: "Customer app" }
    RT@{ icon: "sd:tablet", label: "Restaurant tablet / POS" }
    RA@{ icon: "sd:delivery", label: "Rider app" }
  end
  GW@{ icon: "sd:gateway", label: "API gateway<br/>auth, rate limits" }
  subgraph L3["Layer 3 · Read plane"]
    direction LR
    SV["Serviceability<br/>address → restaurants + ETA"]:::service
    MENU@{ icon: "sd:search", label: "Menus + search<br/>live item flags" }
  end
  subgraph L4["Layer 4 · Orders (exact)"]
    direction LR
    ORD["Order service + saga<br/>idempotency key, outbox"]:::core
    PAY["Payments"]:::service
    REST["Restaurant gateway<br/>accept / reject / ready"]:::service
  end
  subgraph L5["Layer 5 · Planning, per city"]
    direction LR
    PRED["Prediction<br/>prep, travel, ETA distribution"]:::core
    ASG["Assignment<br/>rounds every 20 s, batching"]:::hot
    LOC@{ icon: "sd:map-pin", label: "Location service<br/>latest position per rider" }
  end
  subgraph L6["Layer 6 · Data"]
    direction LR
    DB@{ icon: "sd:database", label: "Orders DB<br/>sharded by city" }
    BUS@{ icon: "sd:stream", label: "Event bus<br/>order, rider, location events" }
    PG@{ icon: "sd:card", label: "Payment gateways" }
  end
  L1 ==> GW
  GW -->|"browse"| L3
  GW ==>|"order"| ORD
  ORD --> PAY --> PG
  ORD ==> REST
  ORD ==>|"due for a rider"| ASG
  PRED --> ASG
  LOC --> ASG
  ORD --> DB
  ORD -.-> BUS
  class CA,RT,RA client
  class GW edge
  class LOC cache
  class BUS queue
  class DB store
  class PG external

KEY INSIGHTS:

  • The order service owns the order's state; restaurants, riders and payments report into it, and every change leaves through an outbox onto the event bus
  • Prediction is a dependency of almost everything: serviceability's arrival times, the restaurant's planned ready time, assignment's dispatch time, and the customer's live estimate
  • Assignment runs per city, which is also how the order database is sharded: a city's dinner rush never touches another city's matching

Data Flow: One Order#

You

"Let's follow Kiran's biryani in Koramangala, with the clock running from Pay."

DiagramOne order, tap to door
%% title: One order, tap to door
sequenceDiagram
  autonumber
  participant C as Kiran's app
  participant O as Order saga
  participant R as Restaurant
  participant A as Assignment
  participant D as Rider
  C->>O: Pay (key k1), ETA shown 34 min
  O-->>C: 0:05 · Order placed
  O->>R: New order on the tablet
  R-->>O: 0:50 · Accepted, ready in 15 min
  Note over O,A: Planned ready 15:50 · rider ~6 min away · dispatch due ~8:20
  O-)A: Due for a rider at 8:20
  A->>D: 8:25 · Offer: pickup 1.6 km, drop 3.4 km
  D-->>A: Accepted
  D->>R: 15:05 · Arrived
  R-->>D: 16:40 · Food handed over
  D->>C: Live location every 4 s, ETA refreshed
  D-->>O: 30:10 · Delivered
  O-->>C: Delivered · rate your order

MEANWHILE, IN PARALLEL:

  • Between minute 1 and minute 8, the rider who eventually got Kiran's order finished another delivery nearby
  • The prediction service re-estimated the ready time twice as the kitchen's other orders were marked ready
  • Thousands of customers browsed the same restaurant's menu from the read plane without touching the order service
Quick check3 questions · +5 XP each

High-level design

  1. Q1/3 Why is assignment run per city rather than as one national service?

    Answer & explanation

    Answer: C

    Riders only carry orders near them, so cities are independent matching problems. Running and sharding per city keeps one city's dinner peak or rainstorm inside that city.

  2. Q2/3 The restaurant accepts at 0:50 with a 15-minute prep time. Why does the saga wait until ~8:20 to ask for a rider?

    Answer & explanation

    Answer: B

    Dispatch time = planned ready time − predicted ride to the restaurant − a small buffer. Riders spend those minutes on other orders.

  3. Q3/3 Why does the order service publish its changes through an outbox instead of calling assignment and the restaurant gateway directly?

    Answer & explanation

    Answer: C

    If the order commits but the call to assignment fails, the order would never get a rider. Writing the event in the same transaction, and relaying it, makes "accepted" and "due for a rider" impossible to lose.

Phase 4 · Stage 4 of 7 · 20 min budget · 15 min read

Phase 4: Deep Dives (20 minutes)#

Deep Dive 1: The Kitchen Is a Partner, Not a Store (Week 2 Concepts: Timeouts, Week 5: Saga)#

You

"In quick commerce the store is ours and we can count the shelf. Here the kitchen belongs to someone else. It can be busy, out of gas, out of an item, or simply not looking at the tablet. Every step with the restaurant needs a timeout and a defined way out."

Your turn · checkpoint 3not answered

The restaurant hasn't answered

Ninety seconds after payment, the restaurant hasn't accepted. Forty seconds later it accepts, then ten minutes in it marks the biryani out of stock. Separately, it says "ready in 15 minutes" but its last twenty orders took 25. How should the system handle each?

Think it through, or jot it down. Then compare.

One strong answer

  • Acceptance timeout: if no answer in ~90 s, ring the restaurant's phone (automated call), and after a hard limit (say 3 minutes) cancel and refund automatically, with a clear message. The customer must not wait 20 minutes to learn the kitchen never saw the order.
  • Late accept after cancel: the saga's state is the truth. A late "accept" for a cancelled order gets "already cancelled" back, so the kitchen doesn't cook it.
  • Item out of stock after acceptance: offer the customer a choice (replace, remove the item and refund it, or cancel), with a short timeout defaulting to refunding the item; mark the item out of stock on the menu at once so nobody else orders it.
  • Prep time the kitchen states vs history: don't trust either alone. Predict from the stated time, that restaurant's history at this hour and its current queue (orders accepted but not yet ready); plan dispatch on the prediction.
  • Restaurant-driven cancellations as a signal: a restaurant that cancels repeatedly (no gas, no stock) is paused from serviceability for a while, so new customers stop ordering there.

The Challenge:

How a kitchen breaks an order
  • Never accepts: the tablet is muted, the staff are busy, the restaurant closed early
  • Accepts and then can't cook: an item is out, the gas ran out, the oven broke
  • Accepts with an optimistic prep time, then runs 10 minutes late
  • Marks "ready" before the food is ready, so the rider waits anyway
  • Many restaurants at once: an LPG supply disruption raised restaurant-driven cancellations enough to show up in Swiggy's quarterly results

The Solution:

DiagramOrder state machine (a saga)
%% title: Order state machine (a saga)
stateDiagram-v2
  [*] --> PLACED
  PLACED --> ACCEPTED: restaurant accepts, prep time
  PLACED --> CANCELLED_REFUNDED: rejected, or no answer in 3 min
  ACCEPTED --> RIDER_ASSIGNED: dispatch time reached
  ACCEPTED --> ITEM_ISSUE: item out of stock
  ITEM_ISSUE --> ACCEPTED: customer replaces or removes item
  ITEM_ISSUE --> CANCELLED_REFUNDED: customer cancels
  RIDER_ASSIGNED --> AT_RESTAURANT
  AT_RESTAURANT --> PICKED_UP: food handed over
  PICKED_UP --> DELIVERED
  PICKED_UP --> UNDELIVERED: customer unreachable
  ACCEPTED --> CANCELLED_REFUNDED: restaurant cancels
  PLACED: PLACED · on the tablet, timer running
  ACCEPTED: ACCEPTED · planned ready time
  ITEM_ISSUE: ITEM_ISSUE · customer chooses, 2 min
  CANCELLED_REFUNDED: CANCELLED · refund automatic
  UNDELIVERED: UNDELIVERED · policy refund
  class PLACED,ACCEPTED,RIDER_ASSIGNED,AT_RESTAURANT,PICKED_UP core
  class DELIVERED ok
  class CANCELLED_REFUNDED,UNDELIVERED bad
  class ITEM_ISSUE queue

KEY DESIGN DECISIONS:

  1. EVERY PARTNER STEP HAS A CLOCK — Accept within 3 minutes, answer an item issue within 2, mark ready within the predicted window. When a clock runs out, the saga moves on by itself: nudge, call, cancel and refund.
  2. THE SAGA'S STATE WINS — A late message from a restaurant or rider is checked against the current state. Accepting a cancelled order, or picking up an order reassigned to someone else, is refused.
  3. PREDICT READY TIME, DON'T COPY IT — The restaurant's stated time is one input. History and the kitchen's current queue are the others. Dispatch uses the prediction.
  4. ONE SIGNAL, MANY USES — Acceptance time, cancellations and "ready" accuracy per restaurant feed back into serviceability (pause unreliable kitchens) and prediction (adjust their times).
  5. REFUNDS ARE PART OF THE SAGA — Every cancelling transition carries its refund, keyed on the order, so a retried cancel never refunds twice.
pythonOrder · 73 lines
# orders/restaurant_steps.py

"""
The restaurant side of the order saga. Every step waits for the
restaurant with a deadline; a timer firing moves the order on without
anyone watching. Messages that arrive late are checked against state.
"""

from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Optional

ACCEPT_NUDGE = timedelta(seconds=90)      # ring the restaurant
ACCEPT_LIMIT = timedelta(minutes=3)       # then cancel and refund
ITEM_ISSUE_LIMIT = timedelta(minutes=2)   # then refund the missing item


@dataclass
class Order:
    order_id: str
    restaurant_id: str
    state: str
    placed_at: datetime
    stated_prep_min: Optional[int] = None
    planned_ready_at: Optional[datetime] = None


class RestaurantSteps:
    def __init__(self, orders, timers, phone, payments, predictor, restaurants, outbox):
        self.orders = orders
        self.timers = timers
        self.phone = phone
        self.payments = payments
        self.predictor = predictor
        self.restaurants = restaurants
        self.outbox = outbox

    def on_placed(self, order: Order) -> None:
        self.timers.at(order.placed_at + ACCEPT_NUDGE, "nudge", order.order_id)
        self.timers.at(order.placed_at + ACCEPT_LIMIT, "accept_timeout", order.order_id)

    def on_timer(self, kind: str, order_id: str) -> None:
        order = self.orders.get(order_id)
        if kind == "nudge" and order.state == "PLACED":
            self.phone.call_restaurant(order.restaurant_id, order_id)
        elif kind == "accept_timeout" and order.state == "PLACED":
            self._cancel(order, reason="RESTAURANT_NO_RESPONSE")

    def on_accept(self, order_id: str, stated_prep_min: int, now: datetime) -> str:
        with self.orders.transaction() as tx:
            order = self.orders.get(order_id, for_update=True, tx=tx)
            if order.state != "PLACED":
                return "ALREADY_" + order.state          # e.g. accepted after we cancelled
            ready = self.predictor.ready_at(order, stated_prep_min, now)
            order.state, order.stated_prep_min, order.planned_ready_at = "ACCEPTED", stated_prep_min, ready
            self.orders.save(order, tx=tx)
            # Assignment hears about it through the outbox, never by a direct call.
            self.outbox.add(tx, "order.accepted", {"order_id": order_id, "ready_at": ready.isoformat()})
        return "OK"

    def on_reject_or_cancel(self, order_id: str, reason: str) -> None:
        order = self.orders.get(order_id)
        if order.state in ("PLACED", "ACCEPTED", "ITEM_ISSUE"):
            self._cancel(order, reason=reason)
            self.restaurants.record_cancellation(order.restaurant_id, reason)

    def _cancel(self, order: Order, reason: str) -> None:
        with self.orders.transaction() as tx:
            order.state = "CANCELLED_REFUNDED"
            self.orders.save(order, tx=tx)
            self.outbox.add(tx, "order.cancelled", {"order_id": order.order_id, "reason": reason})
        # Keyed on the order: a retried cancel can never refund twice.
        self.payments.refund_full(order.order_id, idempotency_key=f"refund:{order.order_id}")
pythonpredict_ready_at · 16 lines
# prediction/ready_time.py

from datetime import timedelta


def predict_ready_at(stated_prep_min, history, queue_len, now):
    """
    Combine three signals into one planned ready time:
    - what the kitchen says,
    - how late this kitchen usually runs at this hour (history.median_overrun_min),
    - how many accepted-but-not-ready orders it has right now.
    A real system learns the weights; the shape of the idea is this.
    """
    base = stated_prep_min + history.median_overrun_min
    load = max(0, queue_len - history.typical_queue) * history.min_per_extra_order
    return now + timedelta(minutes=base + load)

Deep Dive 2: Just-in-Time Assignment and Batching (Week 1 Concepts: Partitioning, Week 8: Streaming vs Batch)#

You

"Ride-hailing assigns the nearest driver the moment a ride is requested, because the passenger is ready now. Food is ready later, at a time we predict. So the question is not only 'which rider' but 'when'."

Your turn · checkpoint 4not answered

Greedy or batched?

At 8:41:00, three orders become due for a rider in one neighbourhood, and three riders are free nearby. If you assign each order the moment it becomes due to its nearest rider, what goes wrong? What does batching the decision every 20 seconds fix, and when should two orders share one rider?

Think it through, or jot it down. Then compare.

One strong answer

  • Greedy goes wrong in order: the first order grabs the rider who is closest to it, who may also be the only rider close to the second order. The second then gets a rider four kilometres away. Locally best, globally worse.
  • Batch the decision: every 20 s, take all orders due in this city and all riders free or finishing soon, score every pair (time to the restaurant against the ready time, the rider's idle time, the order's lateness, the trip's pay), and solve the assignment for the whole set. A few seconds of delay buys a much better match.
  • Batch two orders when nobody pays for it: same restaurant, or two restaurants a few hundred metres apart, drops in the same direction, and both orders' predicted arrival still within their promise. Otherwise, one order per trip.
  • Offer, then confirm: an assignment is an offer the rider can decline; if they do, or don't answer in ~20 s, the order goes back into the next round with a higher priority.

The Challenge:

Why "nearest rider, right now" fails for food
  • The rider arrives early and stands at the counter, or the food waits on the pass
  • First-come assignment gives the best rider to whichever order arrived first, not to the order that needed them most
  • Batching blindly makes the first customer's food wait while the rider fetches the second order
  • Riders decline offers; a match isn't real until it's accepted

The Solution:

DiagramOne assignment round in one city
%% title: One assignment round in one city
flowchart TD
  T@{ icon: "sd:timer", label: "Every 20 s, per city" }
  DUE@{ icon: "sd:queue", label: "Orders due for a rider<br/>dispatch time ≤ now + 20 s" }
  RID@{ icon: "sd:map-pin", label: "Candidate riders<br/>free or finishing soon, ≤ 3 km" }
  PAIR["Candidate pairs + batches<br/>same restaurant or close, same direction"]:::service
  SC["Score each pair<br/>arrival vs ready time, idle, lateness, pay"]:::core
  SOLVE["Solve the assignment<br/>whole round at once"]:::hot
  OFF@{ icon: "sd:bell", label: "Offers to riders<br/>20 s to accept" }
  BACK["Declined or no answer<br/>back next round, higher priority"]:::bad
  T --> DUE
  T --> RID
  DUE --> PAIR
  RID --> PAIR
  PAIR --> SC --> SOLVE --> OFF
  OFF -.-> BACK
  BACK -.-> DUE
  class T cache
  class DUE queue
  class RID cache
  class OFF client

KEY DESIGN DECISIONS:

  1. DISPATCH TIME, NOT ORDER TIME — An order enters a round when planned_ready_at − predicted ride to the restaurant − buffer is near. Orders far from due wait; riders keep moving.
  2. ROUNDS, NOT A STREAM OF GREEDY CHOICES — 20-second rounds per city make the decision for everything due at once. The round runs in seconds; the target for finishing it is one minute.
  3. A COST THAT MEANS SOMETHING — Minutes of rider idle at the restaurant, minutes of food waiting, minutes late against the promise, plus a small fairness term so the same riders don't get all the short trips.
  4. BATCHES ARE CANDIDATES TOO — A two-order batch is scored like a single order, with both orders' arrival times checked against their promises. If either breaks, the batch isn't offered.
  5. NO COUNTDOWN — The offer shows distance and pay. The plan never assumes a rider will ride faster than the model's speed for that road and hour.
pythonJob · 63 lines
# assignment/round.py

"""
One assignment round for one city: score every due order (or two-order
batch) against every candidate rider, then solve the whole round at once.
Costs are minutes; lower is better.
"""

from dataclasses import dataclass
from typing import Dict, List, Tuple

import numpy as np
from scipy.optimize import linear_sum_assignment

BIG = 10_000.0   # "not allowed"


@dataclass
class Job:
    job_id: str              # one order, or a batch of two
    orders: List[str]
    ready_in_min: float      # predicted time until food is ready
    slack_min: float         # minutes before the (latest) promise would break


@dataclass
class Rider:
    rider_id: str
    free_in_min: float       # 0 if free, else minutes until the current drop
    recent_trips: int        # for fairness


def job_cost(job: Job, rider: Rider, travel_min: float, extra_drive_min: float) -> float:
    arrive = rider.free_in_min + travel_min
    rider_idle = max(0.0, job.ready_in_min - arrive)        # rider waits for food
    food_wait = max(0.0, arrive - job.ready_in_min)         # food waits for rider
    late = max(0.0, food_wait + extra_drive_min - job.slack_min)
    if late > 0 and len(job.orders) > 1:
        return BIG                                          # never batch into lateness
    return rider_idle * 1.0 + food_wait * 2.0 + late * 10.0 + rider.recent_trips * 0.2


def run_round(jobs: List[Job], riders: List[Rider], travel: Dict[Tuple[str, str], float],
              extra: Dict[str, float]) -> List[Tuple[str, str]]:
    """Returns (job_id, rider_id) offers. Jobs left unmatched go to the next round."""
    if not jobs or not riders:
        return []
    cost = np.full((len(jobs), len(riders)), BIG)
    for i, j in enumerate(jobs):
        for k, r in enumerate(riders):
            t = travel.get((r.rider_id, j.job_id))           # only pairs within ~3 km exist
            if t is not None:
                cost[i, k] = job_cost(j, r, t, extra.get(j.job_id, 0.0))
    rows, cols = linear_sum_assignment(cost)                  # whole round, not greedy
    offers = [(jobs[i].job_id, riders[k].rider_id) for i, k in zip(rows, cols) if cost[i, k] < BIG]
    # An order may appear alone and inside a batch; keep each order in only one offer.
    taken, final = set(), []
    for job_id, rider_id in sorted(offers, key=lambda o: cost[[j.job_id for j in jobs].index(o[0])].min()):
        orders = next(j.orders for j in jobs if j.job_id == job_id)
        if taken.isdisjoint(orders):
            taken.update(orders)
            final.append((job_id, rider_id))
    return final

A few details carry the design:

  • Travel times come from the prediction service, by road and hour, not straight-line distance; the candidate set is pruned by a geospatial index first, so only pairs within ~3 km are scored.
  • Food waiting costs twice rider waiting in this sketch: cold food hurts the customer and the promise; an idle rider costs the platform minutes.
  • An offer is not an assignment until the rider accepts. The order saga moves to RIDER_ASSIGNED only then, and only if the order is still unassigned (the same state check as Deep Dive 1).
Quick check4 questions · +5 XP each

Kitchens and assignment

  1. Q1/4 A restaurant accepts an order 40 seconds after the system cancelled it for not answering. What should happen?

    Answer & explanation

    Answer: C

    The saga's state is the truth. Checking every late message against it keeps the customer's refund, the kitchen and the rider plan consistent.

  2. Q2/4 Why plan dispatch on a predicted ready time rather than the time the restaurant states?

    Answer & explanation

    Answer: B

    A kitchen that usually runs 8 minutes late at dinner will probably do so tonight. Combining its own estimate with its record and its live queue gets riders there when the food is.

  3. Q3/4 Three orders become due at once and three riders are nearby. Why solve them together instead of giving each order its nearest rider as it arrives?

    Answer & explanation

    Answer: C

    Greedy choices are locally best and globally worse. Scoring all pairs and solving the round as a whole minimises total waiting and lateness.

  4. Q4/4 When should two orders share one rider?

    Answer & explanation

    Answer: D

    A batch saves a trip only if nobody pays for it. If either order would break its promise, the batch costs "BIG" in the round and is never offered.

Deep Dive 3: The Arrival Time Is a Distribution (Week 8 Concepts: Analytics Pipelines, Week 10: SLOs)#

You

"The number on the customer's screen is the product. It's built from guesses, prep time, the rider's ride to the restaurant, the handover and the last mile, each of which varies. So don't predict one number; predict the spread, and choose where in it to promise."

Your turn · checkpoint 5not answered

Which number do you show?

Your model says this order will take 31 minutes on average, but one time in five orders like it take more than 38. Your target is that 90% of orders arrive before the shown time plus a small buffer, in every zone of every city. What do you show, and why would one national rule fail?

Think it through, or jot it down. Then compare.

One strong answer

  • Predict a distribution, not a point: for each order, the model outputs the whole spread of likely delivery times (for example the parameters of a distribution), not just a mean.
  • Promise a quantile: to keep the promise for 90% of orders, show roughly the 90th percentile of that order's distribution (minus the allowed buffer), not the mean. Here that might be ~40 minutes, not 31.
  • Pick the quantile per zone: a zone with heavy traffic variance needs a higher quantile than a quiet one to hit the same 90%. Calibrate the quantile per zone and hour from yesterday's results, and adjust as tonight's results come in.
  • Why one rule fails: a single city-wide or national buffer added to a point estimate over-promises in variable zones and under-promises in steady ones. Swiggy's engineering team described exactly this problem with static city-level buffers, and their move to a model that predicts the full delivery-time distribution per order.
  • Watch both sides: keep the share kept (did it arrive in time?) and the average overestimate (how much longer than reality did we say?). Too high a quantile also costs: customers don't order when they're told 50 minutes.

The Challenge:

The two ways an estimate fails
Too short

The customer was told 25 minutes and waited 40. Trust drops, and a late order often becomes a complaint or a refund.

Too long

The customer was told 50 minutes for a 30-minute delivery, and ordered somewhere else. Over-promising is invisible on a dashboard of "late orders" and shows up only as lost demand.

Uneven

A national average of "90% on time" can hide one zone at 70% every evening in the rain.

The Solution:

DiagramFrom signals to the number on the screen
%% title: From signals to the number on the screen
flowchart TD
  F@{ icon: "sd:stream", label: "Live signals<br/>kitchen queue, riders nearby, traffic, rain" }
  H@{ icon: "sd:database", label: "History<br/>restaurant, zone, hour, dish" }
  M@{ icon: "sd:ml", label: "Delivery-time model<br/>outputs a distribution per order" }
  Q["Quantile per zone and hour<br/>calibrated to keep 90%"]:::core
  ETA["Shown estimate<br/>before checkout, then live"]:::hot
  FB@{ icon: "sd:chart", label: "Results<br/>kept %, average overestimate" }
  F --> M
  H --> M
  M --> Q --> ETA
  ETA -.->|"actual arrival"| FB
  FB -.->|"recalibrate"| Q
  class F queue
  class H store
  class M service
  class FB cache
pythonPrediction · 43 lines
# eta/promise.py

"""
Turn a per-order delivery-time distribution into the number we show.
The model gives a distribution; the business picks the quantile per
zone and hour so that each zone keeps its promise for ~90% of orders.
"""

from dataclasses import dataclass
from statistics import NormalDist
from typing import Dict, List, Tuple


@dataclass
class Prediction:
    mean_min: float
    sd_min: float            # this sketch uses a normal; real systems use richer shapes


class PromiseCalibrator:
    TARGET_KEPT = 0.90
    BUFFER_MIN = 3.0         # "kept" = arrived before shown time + buffer
    STEP = 0.01

    def __init__(self):
        self.q: Dict[Tuple[str, int], float] = {}     # (zone, hour) -> quantile

    def shown_minutes(self, zone: str, hour: int, p: Prediction) -> int:
        q = self.q.get((zone, hour), 0.85)
        return round(NormalDist(p.mean_min, p.sd_min).inv_cdf(q))

    def recalibrate(self, zone: str, hour: int, results: List[Tuple[int, float]]) -> float:
        """results: (shown_min, actual_min) for recent orders in this zone and hour."""
        if len(results) < 200:
            return self.q.get((zone, hour), 0.85)      # too few orders to move the dial
        kept = sum(1 for shown, actual in results if actual <= shown + self.BUFFER_MIN) / len(results)
        q = self.q.get((zone, hour), 0.85)
        if kept < self.TARGET_KEPT:
            q = min(0.98, q + self.STEP)               # promising too little time
        elif kept > self.TARGET_KEPT + 0.04:
            q = max(0.60, q - self.STEP)               # promising too much time: lost orders
        self.q[(zone, hour)] = q
        return q
You

"The same distribution drives more than the screen. Serviceability hides a restaurant when its estimate for this address passes a limit, assignment uses the ready-time part of it, and on a rainy night, when the whole distribution shifts right, the shown times grow honestly instead of riders being pushed."

Deep Dive 4: Thirty Thousand Pings a Second (Week 3 Concepts: Streams, Week 4: Caching)#

You

"Every rider online sends a location every few seconds. Assignment needs the latest position of riders near a restaurant; the customer needs one rider's position on a map; and nobody needs every ping forever."

Your turn · checkpoint 6not answered

Where does a ping go?

120,000 riders send a ping every 4 seconds. Assignment asks "who is within 3 km of this restaurant?" thousands of times a round; 200,000 customers are watching a rider on a map. How do you store the positions, answer both questions cheaply, and stop a stale or out-of-order ping from moving a rider backwards?

Think it through, or jot it down. Then compare.

One strong answer

  • Latest position in memory, by rider: one small record per rider (position, timestamp, status), ~8 MB for the whole fleet, partitioned by city. Each ping overwrites it only if its timestamp is newer, so out-of-order pings can't move a rider back.
  • A geospatial index for "who is near": riders bucketed by cell (an H3 cell or a geohash prefix); a query checks the restaurant's cell and its neighbours. Updated only when a rider changes cell, not on every ping.
  • The trail as a stream: every ping also goes to an append-only stream for trip paths, distance-based pay, disputes and model training, kept hot for days and then summarised.
  • Customers get pushes, throttled: a customer's map subscribes to one rider; the location service pushes at most one update every few seconds, with the refreshed estimate. No customer polls the location store.
  • Treat silence as a signal: no ping for ~60 s marks the rider "unknown"; the order shows the last known position and a watcher decides whether to contact the rider or reassign.
DiagramRider location pipeline
%% title: Rider location pipeline
flowchart TD
  RA@{ icon: "sd:delivery", label: "Rider app<br/>ping every 4 s" }
  IN["Ingest<br/>validate, drop older-than-latest"]:::service
  LAST@{ icon: "sd:cache", label: "Latest position per rider<br/>~8 MB, by city" }
  GEO@{ icon: "sd:hexagons", label: "Cell index<br/>riders per cell" }
  TR@{ icon: "sd:stream", label: "Trail stream<br/>trips, pay, disputes" }
  ASG["Assignment<br/>who is near this restaurant?"]:::hot
  PUSH@{ icon: "tech:websocket", label: "Push to customer map<br/>≤ 1 update / 3 s" }
  RA ==> IN
  IN ==> LAST
  IN --> TR
  LAST --> GEO --> ASG
  LAST --> PUSH
  class RA client
  class TR queue
  class PUSH edge
pythonPosition · 55 lines
# location/ingest.py

"""
Rider pings: keep only the newest position per rider, move the rider
between cells only when the cell changes, and push to watching
customers at a bounded rate.
"""

from dataclasses import dataclass
from typing import Dict, Optional, Set

PUSH_EVERY_MS = 3_000
SILENT_AFTER_MS = 60_000


@dataclass
class Position:
    lat: float
    lng: float
    ts_ms: int
    cell: str


class LocationService:
    def __init__(self, cell_of, trail, pusher):
        self.cell_of = cell_of                     # (lat, lng) -> cell id
        self.trail = trail                         # append-only stream
        self.pusher = pusher
        self.latest: Dict[str, Position] = {}
        self.cells: Dict[str, Set[str]] = {}
        self.last_push: Dict[str, int] = {}

    def on_ping(self, rider_id: str, lat: float, lng: float, ts_ms: int) -> None:
        self.trail.append(rider_id, lat, lng, ts_ms)           # every ping, for the record
        cur: Optional[Position] = self.latest.get(rider_id)
        if cur and ts_ms <= cur.ts_ms:
            return                                             # late or duplicate: ignore
        cell = self.cell_of(lat, lng)
        if cur is None or cur.cell != cell:
            if cur:
                self.cells.get(cur.cell, set()).discard(rider_id)
            self.cells.setdefault(cell, set()).add(rider_id)
        self.latest[rider_id] = Position(lat, lng, ts_ms, cell)
        if ts_ms - self.last_push.get(rider_id, 0) >= PUSH_EVERY_MS:
            self.pusher.to_watchers_of(rider_id, {"lat": lat, "lng": lng, "ts": ts_ms})
            self.last_push[rider_id] = ts_ms

    def near(self, cells: Set[str], now_ms: int) -> Set[str]:
        """Riders in these cells who are not silent."""
        out = set()
        for c in cells:
            for r in self.cells.get(c, ()):
                if now_ms - self.latest[r].ts_ms < SILENT_AFTER_MS:
                    out.add(r)
        return out
Quick check4 questions · +5 XP each

ETA and location

  1. Q1/4 The model says an order will take 31 minutes on average, and the target is that 90% of orders arrive before the shown time plus a buffer. What should the app show?

    Answer & explanation

    Answer: B

    Showing the mean breaks the promise about half the time. Promising a quantile of the order's own spread, calibrated per zone, hits the target without padding every order.

  2. Q2/4 Why calibrate the quantile per zone and hour instead of using one city-wide buffer?

    Answer & explanation

    Answer: B

    A steady zone needs little margin, a congested one needs more. Calibrating per zone and hour lets each keep ~90% without inflating everyone's estimate.

  3. Q3/4 A rider's pings arrive out of order: 8:41:12 arrives after 8:41:16. What should the location service do with the 8:41:12 ping?

    Answer & explanation

    Answer: C

    The latest position must only move forward in time. The trail keeps every ping for pay and disputes; the live position and the cell index ignore older pings.

  4. Q4/4 Why does the cell index update only when a rider changes cell, not on every ping?

    Answer & explanation

    Answer: C

    At ~30,000 pings a second, rewriting the index each time is wasteful. A rider changes cell far less often than every 4 seconds, and "who is near" only needs the cell.

Phase 5 · Stage 5 of 7 · 5 min budget · 3 min read

Phase 5: Scaling and Edge Cases (5 minutes)#

Interviewer

"Walk me through a city's evening, and then tell me what breaks."

A Weeknight in One City#

A weeknight in one large city (illustrative)
  • 6:00 PM · Warm-up
    • Forecast tonight's orders per zone and hour from history, weather and the cricket schedule
    • Incentives and nudges bring riders online ahead of the peak in zones that will need them
  • 8:00-10:00 PM · Dinner peak
    • ~12% of the day's orders in the busiest hour (design assumption); assignment rounds every 20 s, batching more where restaurants cluster
    • Prediction recalibrates quantiles per zone each hour; serviceability hides far restaurants whose estimates pass the limit
  • A shower at 9:10 PM
    • Demand jumps and rides slow; some riders go offline
    • The delivery-time distribution shifts right: shown times grow, the serviceable radius shrinks, batching rises where it still keeps promises
  • 10:00 PM-1:00 AM · Late night
    • Fewer restaurants open, fewer riders; zones merge for assignment so rounds still have riders to choose from
  • Overnight · Learn
    • Retrain prep-time and delivery-time models on today's orders; flag restaurants whose "ready" was often early or late

Edge Cases#

Edge cases
Edge case 1: The rider goes silent mid-delivery

Problem: No ping for two minutes after pickup; the rider might be in a basement, out of battery, or in trouble.

Solution:

  • Show the last known position and "updating"; call the rider through the app
  • If the rider is unreachable for a set time, a support agent takes over; the customer gets an honest update and, if needed, a refund
  • Never reassign food that is already in a bag with a silent rider without a person checking
Edge case 2: The rider declines, again and again

Problem: A far-away restaurant's order is declined by three riders in a row.

Solution:

  • Each decline puts the order back into the next round with higher priority and a higher pay offer for that trip
  • If it can't be placed within its window, the customer's estimate is updated, and after a limit the order is cancelled and refunded rather than left to go cold
Edge case 3: Many restaurants can't cook

Problem: An LPG shortage, a power cut or a festival closure makes many kitchens cancel at once.

Solution:

  • Rising restaurant-driven cancellations pause those restaurants in serviceability within minutes, so new customers don't order there
  • Already-placed orders are cancelled with automatic refunds and a clear reason
Edge case 4: Payment pending after the order reached the kitchen

Problem: UPI says "pending"; the restaurant must not start cooking unpaid food, and the customer must not be charged twice.

Solution:

  • The order is only sent to the restaurant after the payment succeeds; "pending" waits a short, bounded time and is reconciled with the gateway
  • If the money arrives after the order was given up, it is refunded automatically
Edge case 5: Location spoofing

Problem: A rider's app reports fake positions to look closer to busy restaurants or to claim distance pay.

Solution:

  • Plausibility checks on the trail (impossible speeds, jumps, mock-location flags from the device)
  • Pay for distance computed from the road route between pickup and drop, not from the rider's own trail alone

Graceful Degradation#

The degradation ladder, cheapest first
  1. Stretch the estimate
    • Higher quantiles where the distribution has shifted; everyone is told the truth
  2. Batch more
    • Allow more two-order trips, still only when both promises hold
  3. Shrink the radius
    • Hide far restaurants for an address; nearby restaurants stay
  4. Raise rider supply
    • Higher pay for trips in short zones, to bring more riders online
  5. Simplify the app
    • Cached restaurant lists instead of personalised ranking; keep search, checkout and tracking
  6. Pause a zone
    • "Not delivering here right now" rather than accepting orders that will arrive cold
Quick check3 questions · +5 XP each

Scaling and edge cases

  1. Q1/3 It starts raining at 9:10 PM. Rides slow and some riders go offline. Which change comes first?

    Answer & explanation

    Answer: A

    Stretching the estimate costs least and keeps everyone served. Shrinking the radius and pausing zones come later as the gap between demand and riders grows.

  2. Q2/3 An order for a far restaurant has been declined by three riders. What is the right next step?

    Answer & explanation

    Answer: C

    Declines are information: the trip isn't attractive. Raise its priority and pay; if it still can't be placed inside its window, an honest cancellation beats cold food.

  3. Q3/3 Why send the order to the restaurant only after the payment succeeds?

    Answer & explanation

    Answer: C

    A pending payment is an unknown, not a success. Waiting a bounded time and reconciling avoids food that nobody pays for, and late money is refunded automatically.

Phase 6 · Stage 6 of 7 · 2 min

Phase 6: Monitoring and Operations#

You

"Food delivery fails locally: one zone, one evening, one kitchen. The dashboard has to show cities and zones, not just national totals."

Monitoring Dashboard#

Bengaluru — Saturday 9:24 PM (illustrative)
Orders and promise
Metric Now
Orders, last 10 minutes 7,410
Promise kept, last hour (city) 91.2%
Worst zone, promise kept 84.7% (rain)
Order-to-door p50 / p90 31 / 44 min
Restaurants
  • Accept time p50 / p90: 38 s / 2 min 10 s
  • Restaurant-driven cancellations: 1.4%
  • Ready-time error p50: +2.1 min (kitchens later than predicted)
Riders and assignment
  • Riders online: 18,240; utilisation 78%
  • Round time p99: 6.8 s (target < 60 s)
  • Offers declined: 11%; two-order trips: 13%
  • Rider wait at restaurants p50: 2.4 min
Alerts (last hour)
  • 20:58 (amber) — Rain in the south-east zones; ride-time model multiplier raised, quantiles up one step
  • 21:10 (amber) — Zone SE-4 promise kept below 85%, serviceable radius reduced to 4 km
  • 21:19 (amber) — Restaurant R-88213 cancelled 4 orders in 15 minutes, paused for 30 minutes

SLOs for the Platform#

SLOs for food delivery
SLO 1: Promise kept
  • Target: ≥ 90% of orders delivered before the shown time plus buffer, per zone, per hour (design target)
SLO 2: No silent orders
  • Target: every order gets a restaurant decision within 3 minutes, or is cancelled and refunded automatically
SLO 3: Assignment
  • Target: every city's round completes in < 60 s p99; every due order offered within its window
SLO 4: Money integrity
  • Target: 100% of captured payments end in a delivered order or a completed refund within 30 minutes of cancellation
  • Error budget: 0
SLO 5: Rider fairness
  • Target: median rider wait at restaurants < 5 minutes per zone per hour; no offer shown with a countdown to the customer's promise
Quick check3 questions · +5 XP each

Reading the dashboard

  1. Q1/3 The city keeps 91.2% of promises, but zone SE-4 is at 84.7%. What does the system do?

    Answer & explanation

    Answer: B

    The SLO is per zone because that is where customers feel it. A healthy city average can hide one zone failing every order in the rain.

  2. Q2/3 Ready-time error is +2.1 minutes: kitchens are later than predicted. What is the cost if nothing changes?

    Answer & explanation

    Answer: B

    A late kitchen turns the planned just-in-time arrival into rider idle time. Feeding the error back into the ready-time model moves dispatch later and keeps the estimate honest.

  3. Q3/3 Why pause a restaurant that cancelled 4 orders in 15 minutes?

    Answer & explanation

    Answer: C

    Repeated restaurant cancellations predict more. Pausing it for a while protects customers and riders from orders that will fail, and it comes back automatically.

Final challenge · Stage 7 of 7 · 2 min

Final Challenge: New Year's Eve, 8 PM, and It's Raining#

The interviewer checks the calendar. "One more. December 31st."

Interviewer

"It's 7:45 PM on New Year's Eve in Bengaluru. Orders are coming in at about seven times an average minute, heavily skewed to biryani, pizza and burgers from a few hundred kitchens. At 8:05 it starts to rain: a quarter of riders go offline and rides take half as long again. What do you do, minute by minute?"

The New Year's Eve brief
  • Demand: ~7× an average minute, and concentrated on a few dishes and kitchens
  • Kitchens: the popular ones have queues of 30+ orders; their prep times double
  • Riders: 25% offline after 8:05; rides 1.5× longer
  • Payments, menus and tracking all at their own peaks
  • Riders must not be pushed to ride faster on wet roads
Your turn · checkpoint 7not answered

Protect the promise, the kitchens and the riders

Where is the bottleneck at 7:45, and where is it at 8:15? What do you change in serviceability, prediction, assignment and the app?

Think it through, or jot it down. Then compare.

One strong answer

  • 7:45, the kitchens: the busiest restaurants are the bottleneck. The ready-time model sees their queues and pushes their planned ready times out, so dispatch waits and riders don't pile up at counters. Serviceability raises their estimates; past a queue limit, they stop taking orders for a few minutes ("busy, back soon") instead of accepting food they can't cook for an hour.
  • 8:15, the riders: after the rain, riders are the bottleneck. Each trip takes ~1.5× as long and there are 25% fewer riders, so capacity per hour falls to about half (0.75 ÷ 1.5). Quantiles jump per zone, far restaurants drop out of each address's list first, and batching rises where pickups cluster.
  • Assignment: rounds stay at 20 s; the cost function now weighs lateness more, and declined far trips are re-offered with higher pay or given up with a refund before the food goes cold.
  • Rider supply and safety: higher pay for trips in the short zones brings riders back online; no countdowns, safety prompts in the rider app, and the promise grows instead of speeds.
  • Shed load in the app: cached restaurant lists instead of personalised ranking; keep checkout, payments and tracking at full capacity.
  • Plan next year: New Year's Eve is on the calendar. Pre-agree order caps with the biggest kitchens, roster riders, and open the evening with wider estimates rather than discovering the queue at 7:50.
Quick check4 questions · +5 XP each

Final challenge · Quest check

  1. Q1/4 At 7:45 PM the most popular kitchens have queues of 30+ orders. What should serviceability do for them?

    Answer & explanation

    Answer: C

    The kitchen is the bottleneck. Honest, longer estimates and a short "busy" pause stop the platform accepting food the kitchen can't cook in time.

  2. Q2/4 After the rain, 25% of riders go offline and rides take 1.5× as long. Roughly what happens to delivery capacity per hour?

    Answer & explanation

    Answer: C

    0.75 of the riders, each completing trips 1/1.5 as fast: 0.75 ÷ 1.5 = 0.5. That gap is why estimates must grow and the radius must shrink.

  3. Q3/4 Why do far restaurants drop out of an address's list before near ones?

    Answer & explanation

    Answer: B

    When riders are scarce, every rider-minute counts. Long trips cost the most, so shedding them first keeps the most customers inside their promises.

  4. Q4/4 Biryani peaked near 1,336 orders a minute on New Year's Eve against a 2025 average of about 194. About how many times the average is that?

    Answer & explanation

    Answer: B

    1,336 ÷ 194 ≈ 6.9. Peaks like that are why this design sizes for ~200 orders a second on New Year's Eve against ~83 at a normal dinner peak.

Wrap-up · 5 min

Interview Conclusion#

Transcript
Interviewer

"Good. A few quick ones to finish."

Interviewer

"Why not just assign the nearest rider as soon as the order is placed?"

You

"Because food isn't ready yet. The nearest rider would stand at the counter for most of the prep time, and that rider is gone from the pool. Assign when the predicted ride to the restaurant matches the predicted ready time, and decide in batches per city, so one order doesn't take the rider another needed more."

Interviewer

"Where would you spend your next engineering quarter?"

You

"Prep-time prediction. It drives when riders leave, how long they wait, and half of the customer's estimate, and the kitchen is the one part we can't see. Every minute of error there shows up as rider idle time or cold food."

Interviewer

"What's the single most important design decision?"

You

"Treating the arrival time as a distribution and promising a calibrated quantile per zone. It gives every other part a target: the kitchen queue, dispatch timing, batching and the serviceable radius all work to keep that promise, and on a bad night the number grows instead of the rider's speed."

Summary: Concepts Applied from 10-Week Course#

Concepts from the 10-week course in food-delivery design
  • Week 1: Data at scale
    • Partitioning — orders, assignment and location sharded by city
    • Geospatial indexing — riders bucketed by cell, queried by cell and neighbours
  • Week 2: Failure-first design
    • Timeouts — every restaurant and rider step has a clock and a defined way out
    • Idempotency — one key per checkout; refunds keyed on the order
  • Week 3: Messaging and async processing
    • Transactional outbox — "accepted" and "due for a rider" can't be lost
    • Streams — 30,000 location pings a second into a trail and a latest-position store
  • Week 4: Caching
    • Latest rider positions in memory; restaurant lists and menus from caches
  • Week 5: Consistency and coordination
    • Saga — order, restaurant, rider and payment steps with compensation
    • State checks against late messages
  • Week 8: Analytics pipelines
    • Delivery-time distributions and per-zone calibration from yesterday's results
  • Week 10: Production readiness
    • SLOs per zone and hour; a degradation ladder; planning for known peaks

Why Swiggy Matters#

Why this is a different kind of hard problem
Three sides, none of them yours
  • The customer, the restaurant and the rider can each say no. Every step needs a timeout, a fallback and a refund path
Timing, not distance
  • Ride-hailing matches the nearest driver now. Food dispatch matches a rider to a future moment, the predicted ready time, and is only as good as that prediction
A promise built from guesses
  • The arrival time is the product, and it's the sum of things that vary: a kitchen, two rides, a handover. Predicting the spread and promising a calibrated quantile is the honest way to give one number

"The rider isn't late because they rode slowly. They're late because we sent them at the wrong time, to a kitchen we didn't understand."

Self-Assessment Checklist#

After studying this case study, you should be able to:

Architecture:

Architecture0/2 ready
  • Split a food-delivery platform into a read plane, an exact order saga, and per-city planning
  • Trace one order from tap to door with its time budget

The kitchen:

The kitchen0/2 ready
  • Put a timeout and a compensation on every restaurant step
  • Predict ready time from the stated time, history and the kitchen's queue

Assignment:

Assignment0/3 ready
  • Explain why dispatch time, not order time, starts assignment
  • Batch the assignment decision per city and solve it jointly
  • Decide when two orders should share a rider

The promise:

The promise0/2 ready
  • Predict a delivery-time distribution and promise a calibrated quantile per zone
  • Measure both promises kept and over-promising

Operations:

Operations0/2 ready
  • Ingest rider locations at ~30,000 pings a second without moving riders backwards
  • Plan for rain and New Year's Eve with a degradation ladder

Sources#

Company figures:

Orders and peaks:

Engineering (public write-ups):

Gig workers and regulation:

Design note: the architecture, code, timeouts, thresholds, budgets and dashboards in this case study are an illustrative design for the problem, not a description of Swiggy's internal systems beyond what the sources above describe.

Further Reading#

Related systems to study:

  • Read

    Blinkit (Bonus Problem 12 in this series): a store you control and riders anchored to it; compare with kitchens you don't and riders who float

  • Read

    Uber (Bonus Problem 9): city-wide matching on H3 when the passenger is ready now

  • Read

    Razorpay Payment Gateway (Bonus Problem 18): pending payments, reconciliation and refunds from the other side

Foundational concepts (this course):

  • Read

    Week 1, Day 1 — Partitioning Deep-Dive

  • Read

    Week 2, Day 1 — Timeout Hell

  • Read

    Week 3, Day 2 — Transactional Outbox

  • Read

    Week 5, Day 2 — Distributed Transactions: The Saga Pattern

  • Read

    Week 10, Day 1 — SLIs, SLOs and SLAs

"A kitchen you can't see, a rider who could be anywhere, and a customer watching a dot on a map. The system that makes it work doesn't drive faster. It sends the right rider at the right minute, and tells the customer a time it can keep."

End of Side quest 17 · Swiggy Food Delivery
Clear the questFinish Swiggy.Stages clear as you read them. Marking complete unlocks after ~7 more min of reading.
  • 0/7stages cleared
design sheet · 6 diagrams · pdfRevise the whole design in ten minutesThe numbers, architecture, sequence, class and state diagrams, decisions and failure modes of this quest on a few printable pages.open the sheet →

New lessons by email

New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.

Discussion · …

Public: everyone reading this lesson sees comments. Be kind and keep it on the material.

Sign in to ask a question or answer one. Reading stays open to everyone.
loading the discussion…