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.
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.
- ₹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.
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.
"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."
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
"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: Requirements Clarification (5 minutes)#
"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#
"Does the restaurant have to accept each order, or is it automatic?"
"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."
"Do we know how long a dish takes to cook?"
"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."
"Do riders work for a store, like quick commerce, or float around the city?"
"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."
"How big is the order volume?"
"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."
"And the peaks?"
"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."
"How is the arrival time judged?"
"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#
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#
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: Back of the Envelope Estimation (5 minutes)#
"The interesting numbers here are minutes and riders, not requests. Let me do the minutes first."
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?
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#
| 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#
- 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#
- 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#
- 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 |
Requirements and estimation
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.
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.
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: High-Level Design (10 minutes)#
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?
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.
"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#
%% 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 externalKEY 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#
"Let's follow Kiran's biryani in Koramangala, with the clock running from Pay."
%% 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
High-level design
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.
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.
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: Deep Dives (20 minutes)#
Deep Dive 1: The Kitchen Is a Partner, Not a Store (Week 2 Concepts: Timeouts, Week 5: Saga)#
"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."
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?
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:
- 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:
%% 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:
- 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.
- 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.
- 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.
- ONE SIGNAL, MANY USES — Acceptance time, cancellations and "ready" accuracy per restaurant feed back into serviceability (pause unreliable kitchens) and prediction (adjust their times).
- REFUNDS ARE PART OF THE SAGA — Every cancelling transition carries its refund, keyed on the order, so a retried cancel never refunds twice.
# 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}")# 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)#
"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'."
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?
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:
- 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:
%% 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 clientKEY DESIGN DECISIONS:
- DISPATCH TIME, NOT ORDER TIME — An order enters a round when
planned_ready_at − predicted ride to the restaurant − bufferis near. Orders far from due wait; riders keep moving. - 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.
- 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.
- 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.
- 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.
# 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 finalA 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_ASSIGNEDonly then, and only if the order is still unassigned (the same state check as Deep Dive 1).
Kitchens and assignment
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.
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.
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.
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)#
"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."
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?
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:
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:
%% 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# 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"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)#
"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."
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?
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.
%% 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# 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 outETA and location
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.
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.
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.
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: Scaling and Edge Cases (5 minutes)#
"Walk me through a city's evening, and then tell me what breaks."
A Weeknight in One City#
- 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 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#
- Stretch the estimate
- Higher quantiles where the distribution has shifted; everyone is told the truth
- Batch more
- Allow more two-order trips, still only when both promises hold
- Shrink the radius
- Hide far restaurants for an address; nearby restaurants stay
- Raise rider supply
- Higher pay for trips in short zones, to bring more riders online
- Simplify the app
- Cached restaurant lists instead of personalised ranking; keep search, checkout and tracking
- Pause a zone
- "Not delivering here right now" rather than accepting orders that will arrive cold
Scaling and edge cases
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.
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.
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: Monitoring and Operations#
"Food delivery fails locally: one zone, one evening, one kitchen. The dashboard has to show cities and zones, not just national totals."
Monitoring Dashboard#
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#
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
Reading the dashboard
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.
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.
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: New Year's Eve, 8 PM, and It's Raining#
The interviewer checks the calendar. "One more. December 31st."
"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?"
- 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
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?
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.
Final challenge · Quest check
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.
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.
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.
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.
Interview Conclusion#
"Good. A few quick ones to finish."
"Why not just assign the nearest rider as soon as the order is placed?"
"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."
"Where would you spend your next engineering quarter?"
"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."
"What's the single most important design decision?"
"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#
- 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#
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:
- 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:
- Put a timeout and a compensation on every restaurant step
- Predict ready time from the stated time, history and the kitchen's queue
Assignment:
- 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:
- Predict a delivery-time distribution and promise a calibrated quantile per zone
- Measure both promises kept and over-promising
Operations:
- 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:
- Paper
Swiggy Ltd — Q1 FY27 letter to shareholders (food delivery GOV ₹9,490 crore, +17.4% YoY; 19.2 million food MTU; 2,65,800 restaurant partners and 6,48,000 delivery partners transacting monthly; 308 million B2C orders, 114.5 million on Instamart; restaurant-driven cancellations during LPG disruptions), July 2026: https://nsearchives.nseindia.com/corporate/SWIGGY_30072026154100_Covering_Letter_Letter_to_Shareholders_-_signed.pdf
- Read
YourStory — Swiggy losses narrow as revenues rise 37% in Q1 FY27, July 2026: https://yourstory.com/2026/07/swiggy-losses-down-34-as-revenues-rise-37-in-q1-of-fy27
Orders and peaks:
- Read
Swiggy — How India Swiggy'd 2025 (93 million biryanis from 1 January to 30 November 2025, 194 a minute), December 2025: https://www.swiggy.com/corporate/press-release/how-india-swiggyd-2025-a-feast-for-the-fast-the-foodie-and-the-future/
- Read
Storyboard18 — Indians rang in 2026 with biryani, pizza and 1.3K orders a minute on Swiggy (biryani peak near 1,336 a minute just before 8 PM on 31 December 2025), January 2026: https://www.storyboard18.com/brand-marketing/indians-rang-in-2026-with-biryani-pizza-and-1-3k-orders-a-minute-on-swiggy-86860.htm
Engineering (public write-ups):
- Read
Parag Bhandarkar, Swiggy Bytes — Solving regional consistency of delivery ETA with a deep probabilistic framework (compliance as actual arrival before ETA plus buffer; static city-level buffers; a model predicting the full delivery-time distribution per order), October 2026: https://medium.com/swiggy-bytes/solving-regional-consistency-of-delivery-eta-with-a-deep-probabilistic-framework-8d82d6abdf41
- Read
Akka — Swiggy customer story (order assignment scores every order against every eligible rider in a city within a one-minute SLA; 5,000+ predictions a second on one model; p99 prediction latency from 144 ms to 71 ms): https://akka.io/customer-stories/swiggy
Gig workers and regulation:
- Read
Outlook Business — Gig workers secured under new labour codes as aggregators must pay up to 2% of turnover (codes in force 21 November 2025; Aadhaar-linked UAN), November 2025: https://www.outlookbusiness.com/news/gig-workers-secured-under-new-labour-codes-as-aggregators-must-pay-2-of-turnover
- Paper
Press Information Bureau — Labour reforms: formalising and safeguarding India's gig and platform workforce (1-2% of annual turnover, capped at 5% of payments to gig and platform workers), December 2025: https://static.pib.gov.in/WriteReadData/specificdocs/documents/2025/dec/doc2025129723601.pdf
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."
- 0/7stages cleared
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.