At a glance#
| Number | Value | What it drives |
|---|---|---|
| Food customers · restaurants · riders | 19.2M · 2.66 lakh · 6.48 lakh a month | three partners who can each say no |
| Food orders a day (design target) | 2.5 million | ~2.1 million today from public figures |
| Orders at dinner peak · New Year's Eve | ~83 / s · ~200 / s | biryani peaked ~7× an average minute |
| Riders online at dinner | ~120,000 | ~27-minute trips, ~2.5 orders an hour |
| Location pings | ~30,000 / s | latest position in memory, ~8 MB |
| Pairs scored per city round | ~10,000 every 20 s | joint assignment, under a 1-minute target |
| Median order-to-door, ~3.5 km | ~32 min | the ride to the kitchen hides inside cooking |
| Promise target | ≥ 90% kept per zone per hour | quantile calibrated per zone |
Architecture#
DiagramFood delivery on one page
%% title: Food delivery on one page
flowchart TD
CA@{ icon: "sd:phone", label: "Customer app" } ==> GW@{ icon: "sd:gateway", label: "API gateway<br/>auth · rate limits" }
GW --> SV["Serviceability + menus<br/>restaurants that can deliver now · ETA"]:::service
GW ==> ORD["Order saga<br/>idempotency key · outbox · refunds<br/>orders DB sharded by city"]:::core
ORD --> PAY@{ icon: "sd:card", label: "Payment gateways" }
ORD ==> REST["Restaurant gateway<br/>accept ≤ 3 min · item issues · ready"]:::service
REST --> RT@{ icon: "sd:tablet", label: "Restaurant tablet / POS" }
ORD -.-> BUS@{ icon: "sd:stream", label: "Event bus<br/>order + rider events" }
BUS ==>|"due for a rider"| ASG["Assignment per city<br/>20-s rounds · batching"]:::hot
PRED@{ icon: "sd:ml", label: "Prediction<br/>prep · travel · ETA distribution" } --> ASG
PRED --> SV
LOC@{ icon: "sd:map-pin", label: "Location service<br/>latest position per rider" } --> ASG
ASG ==>|"offers"| RA@{ icon: "sd:delivery", label: "Rider app<br/>ping every 4 s" }
RA -.-> LOC
class CA,RT,RA client
class GW edge
class PAY external
class BUS queue
class PRED service
class LOC cache- Exact: the order saga owns each order's state; kitchens, riders and payments report into it.
- Prediction feeds the shown ETA, the planned ready time and the dispatch time.
- Per city: assignment, orders and locations are sharded by city, so one city's rush stays there.
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<br/>ready − ride − buffer ≤ now" }
T --> RID@{ icon: "sd:map-pin", label: "Candidate riders<br/>free or finishing · ≤ 3 km" }
DUE --> PAIR["Pairs + two-order batches<br/>close pickups · same direction"]:::service
RID --> PAIR
PAIR --> SC["Score in minutes<br/>rider idle · food wait · lateness · fairness"]:::core
SC --> SOLVE["Solve the whole round<br/>not greedy"]:::hot
SOLVE --> OFF@{ icon: "sd:bell", label: "Offers · 20 s to accept" }
OFF -.->|"declined"| DUE
class T,RID cache
class DUE queue
class OFF clientKey flows#
DiagramOne order, tap to door
%% title: One order, tap to door sequenceDiagram autonumber participant C as Customer participant O as Order saga participant R as Restaurant participant A as Assignment participant D as Rider C->>O: Pay (key k1) · ETA 34 min O->>R: Order on the tablet R-->>O: 0:50 · accepted, ready in 15 min Note over O,A: planned ready 15:50 · ride to kitchen ~6 min · dispatch due ~8:20 O-)A: Due for a rider (via outbox) A->>D: 8:25 · offer D-->>A: Accepted D->>R: 15:05 · at the kitchen R-->>D: 16:40 · food handed over D-->>C: live location every few seconds D-->>O: 30:10 · delivered
DiagramThe kitchen doesn't answer
%% title: The kitchen doesn't answer
sequenceDiagram
autonumber
participant O as Order saga
participant R as Restaurant
participant P as Payments
participant C as Customer
O->>R: New order · timer starts
Note over O: 90 s · no answer
O->>R: Automated phone call
alt accepted in time
R-->>O: Accepted · prep time
else 3 min, still nothing
O->>P: Full refund (keyed on the order)
O-->>C: Cancelled · refund on its way
R-->>O: Late accept
O-->>R: Already cancelled · don't cook
endData model#
DiagramCore entities
%% title: Core entities
classDiagram
class Order {
+orderId : string
+idemKey : string
+cityId : string · shard key
+state : OrderState
+plannedReadyAt : timestamp
+shownEtaMin : int
}
class Restaurant {
+restaurantId : string
+zone : string
+medianOverrunMin : float
+pausedUntil : timestamp
}
class Rider {
+riderId : string
+status : FREE / ON_TRIP / SILENT
+cell : string
+lastPingAt : epoch ms
}
class Offer {
+offerId : string
+orders : one or two order ids
+expiresIn : 20 s
}
class Ping {
+riderId : string
+lat : float
+lng : float
+ts : epoch ms
}
Order "many" --> "1" Restaurant : cooked by
Offer "1" --> "many" Order : carries
Offer "many" --> "1" Rider : offered to
Rider "1" --> "many" Ping : sends- Orders, assignment and locations are partitioned by city; riders are bucketed by cell for "who is near".
- Orders ~3 KB each, ~7.5 GB a day; the ping trail ~52 GB a day, kept hot for a few days.
Lifecycle#
DiagramOrder state machine
%% title: Order state machine stateDiagram-v2 [*] --> PLACED PLACED --> ACCEPTED: kitchen accepts PLACED --> CANCELLED: rejected or 3 min silence ACCEPTED --> ITEM_ISSUE: item out of stock ITEM_ISSUE --> ACCEPTED: replace or remove item ITEM_ISSUE --> CANCELLED: customer cancels ACCEPTED --> RIDER_ASSIGNED: dispatch time reached RIDER_ASSIGNED --> PICKED_UP: food handed over PICKED_UP --> DELIVERED PICKED_UP --> UNDELIVERED: customer unreachable DELIVERED --> [*] CANCELLED --> [*] UNDELIVERED --> [*] class DELIVERED ok class CANCELLED,UNDELIVERED bad class ITEM_ISSUE queue
Decisions#
| Decision | Chose | Over | Because |
|---|---|---|---|
| When to dispatch | Ready time − ride − buffer | The moment it's accepted | riders don't wait 9 minutes at counters |
| How to match | Joint solve per city round | Greedy nearest rider | first order can't steal the second's rider |
| Batching | Only if both promises hold | Whenever pickups are close | nobody's food waits for the other order |
| Ready time | Predicted from stated + history + queue | Restaurant's number alone | kitchens run late in patterns |
| Shown ETA | Calibrated quantile per zone and hour | Mean + one city-wide buffer | each zone keeps ~90% |
| Kitchen steps | Clock on every step, saga state wins | Waiting for the restaurant | no order left hanging |
| Locations | Newest ping wins, cell index | Store and scan every ping | old pings can't move riders back |
When it breaks#
| Failure | What users see | Contain it with |
|---|---|---|
| Kitchen never accepts | cancelled with a refund in 3 min | timers, automated call, auto refund |
| Many kitchens can't cook | fewer restaurants listed | pause on rising cancellations |
| Rain at dinner | longer, honest ETAs, smaller radius | quantiles up, shrink radius, batch more |
| Rider goes silent mid-trip | last known position, then an update | 60-s silence = unknown, a person decides |
| Far order declined repeatedly | revised ETA, else cancel + refund | re-offer with higher priority and pay |
Say it in the interview#
- Food isn't ready yet: dispatch at ready time minus the ride, not when the order is accepted.
- Batch the decision: score all due orders against all nearby riders every 20 s and solve together.
- The kitchen is a partner: every step has a clock, and a late message is checked against the saga.
- Promise a quantile: show about the 90th percentile of the order's delivery-time spread, calibrated per zone.
- Shard by city: matching is local, so a city's rush can't touch another's.
- The estimate grows, not the speed: in rain, stretch ETAs, shrink the radius, never add rider pressure.