At a glance#
| Number | Value | What it drives |
|---|---|---|
| Dark stores | 2,443 (June 2026) | design for 2,500; shard, queue and promise per store |
| Orders | ~3.6 million / day (331M in Apr–Jun 2026) | design for 4M = 1,600 per store a day |
| One store's busiest hour | ~192 orders, ~3.2 / min | 12 % of the day (assumed): ~8 pickers, ~48 riders |
| National order rate | ~133 / s · ~400 / s on spike nights | small; reads are ~100× (13K–40K / s), own plane |
| Average order | 2.5 min pick + ~2 km at ~15 km/h ≈ 8 min | 10.5 min before any queue or handover |
| A true ten minutes | ~1.5 km of road (~6 min ride) | the lever is store density, not rider speed |
| Stock per store | ~10,000 SKUs (assumed) · ~1 MB | tens of reservations a minute; losses 1.8 % of order value |
| Rider pings at 9 PM | ~30,000 / s | keep only the latest position per rider, in memory |
Architecture#
DiagramQuick commerce on one page: read plane, per-store write plane, store and road
%% title: Quick commerce on one page: read plane, per-store write plane, store and road
flowchart TD
CA@{ icon: "sd:phone", label: "Customer app<br/>~100 reads per order" } --> GW@{ icon: "sd:gateway", label: "API gateway<br/>auth · rate limits" }
GW -->|"browse · search"| SV["Serviceability + catalog<br/>pin → H3 cell → 2–3 stores"]:::service
GW ==>|"checkout"| CO["Checkout + order saga<br/>one idempotency key per attempt"]:::core
SV --> AV@{ icon: "sd:cache", label: "Availability cache<br/>in-stock bitmap per store<br/>seconds stale" }
CO ==> INV["Inventory service<br/>only stock writer · guarded UPDATE"]:::hot
CO --> PG@{ icon: "sd:card", label: "Payment gateways<br/>UPI · cards" }
INV ==> DB@{ icon: "sd:database", label: "Orders + stock DB<br/>sharded by store" }
DB ==>|"outbox"| BUS@{ icon: "sd:stream", label: "Event bus<br/>order + stock events" }
BUS --> PQ@{ icon: "sd:scan", label: "Picker queue + handhelds<br/>earliest pack-by first" }
BUS --> DSP["Dispatch<br/>rider planned during picking · batch 2"]:::service
DSP --> RA@{ icon: "sd:delivery", label: "Rider app<br/>ping every 4 s · no countdown" }
RA --> LOC@{ icon: "sd:map-pin", label: "Rider locations<br/>latest per rider · TTL" }
PQ --> PE["Promise engine, per store<br/>live queue · pickers · riders → ETA + mode"]:::core
class CA client
class GW edge
class AV,LOC cache
class PG external
class DB store
class BUS,PQ queue
class RA client- Two planes. Browsing is ~100× the order rate and may be seconds stale; checkout re-checks stock exactly. Stock events from the bus refresh the in-stock bitmap; the promise engine publishes each store's mode and per-cell estimates to serviceability.
- Everything is per store. A basket comes from one store, so its stock, reservations and orders sit on one shard, and one store's rush never touches its neighbours.
DiagramStore selection: which store serves this pin, right now
%% title: Store selection: which store serves this pin, right now
flowchart TD
PIN@{ icon: "sd:map-pin", label: "Delivery address pin" } --> CELL@{ icon: "sd:hexagons", label: "H3 cell · resolution 9<br/>~0.105 km²" }
CELL --> IDX@{ icon: "sd:cache", label: "Cell index · ~300K entries in memory<br/>cell → 2–3 candidate stores" }
IDX --> MODE{"Store open for this cell?"}
PE["Promise engine, every few s<br/>OPEN → STRETCHED → THROTTLED → CLOSED<br/>outer cells close first · hysteresis"]:::core -.-> MODE
MODE -->|"none open"| NONE["Not serviceable right now<br/>offer a later slot"]:::bad
MODE -->|"yes"| SCORE["Score candidates<br/>1 · cart coverage<br/>2 · backlog + pick + p90 ride"]:::hot
SCORE --> PICK["Serving store + honest estimate<br/>pinned for the session"]:::ok
class PIN client
class CELL service
class IDX cacheKey flows#
DiagramOne order, tap to door
%% title: One order, tap to door sequenceDiagram autonumber participant C as Riya's app participant O as Checkout / saga participant I as Inventory participant P as Payment participant S as Picker participant R as Rider Note over C: before Pay · store chosen, "11 min" shown C->>O: Pay (idempotency key k1) O->>I: Reserve 5 lines at store 412 I-->>O: Reserved · hold 5 min O->>P: Create payment (k1) P-->>O: UPI success O-->>C: 0:04 · confirmed, ~11 min O-)S: Outbox event → picker queue, pack by 3:00 O-)R: Dispatch plans a rider for ~3:00 Note over S: ambient aisles first, cold room last S->>I: Scan each item · stock leaves the shelf S-->>R: 2:55 · bag on rack slot B7 R->>C: 3:10 · picked up, location every 4 s R-->>C: 10:55 · delivered
DiagramPayment unknown, or late: never guess
%% title: Payment unknown, or late: never guess
sequenceDiagram
autonumber
participant O as Order saga
participant I as Inventory
participant P as Payment gateway
participant C as Customer
O->>P: Charge (k1)
P--xO: timeout
Note over O: PAYMENT_PENDING · not a failure
O->>I: Extend hold +10 min
O->>P: Reconcile after 30 s
alt paid, stock still held
P-->>O: SUCCESS
O->>I: Commit hold
O-->>C: Confirmed
else paid, but hold had expired and stock is gone
P-->>O: SUCCESS
O->>I: Try to reserve again · short
O->>P: Refund (refund:order_id)
O-->>C: Refunded, clear message
else failed
P-->>O: FAILED
O->>I: Release hold
O-->>C: Cancelled · nothing charged
endDiagramEmpty bin at the shelf: fix the count, save the order
%% title: Empty bin at the shelf: fix the count, save the order
sequenceDiagram
autonumber
participant S as Picker
participant I as Inventory
participant O as Order saga
participant C as Customer
S->>I: Not found · counted 0 (system said 3)
Note over I: on_hand = 0 · release this line's hold · queue recount
I->>O: Line short
alt customer allowed a substitute
O->>I: Reserve substitute line
O-->>S: Pick substitute
else no substitute
O-->>C: Line refunded
end
Note over S,C: rest of the order carries on · repeat not-founds raise the SKU's bufferData model#
DiagramCore entities
%% title: Core entities
classDiagram
class Store {
+storeId : id · shard key
+mode : OPEN / STRETCHED / THROTTLED / CLOSED
+openCells : H3 cells accepted now
+backlogMin : float
}
class Stock {
+storeId : id
+skuId : id
+onHand : int · drops only at the scan
+reserved : int · promised to unpicked orders
+buffer : int · allowance for count drift
}
class Order {
+orderId : id
+idemKey : string · unique per checkout
+storeId : id
+state : OrderState
+paymentRef : string
+promisedAt : timestamp · p90 estimate
+packBy : timestamp
}
class Reservation {
+orderId : id
+status : HELD / COMMITTED / RELEASED
+expiresAt : timestamp · plus 5 min
}
class OrderLine {
+skuId : id
+qty : int
+ifMissing : substitute / refund
}
Store "1" --> "many" Stock : holds
Store "1" --> "many" Order : serves
Order "1" --> "many" OrderLine : contains
Order "1" --> "1" Reservation : holds stock via
OrderLine "many" --> "1" Stock : reserves fromavailable = on_hand − reserved − buffer. Checkout runsreserved += q WHERE available >= qper line, in one transaction on the store's shard.- Small data: all stock ~2.5 GB (25M rows), orders ~8 GB a day, rider trail ~35 GB a day kept hot for a few days.
Lifecycle#
DiagramOrder state machine: a saga with a way back at every step
%% title: Order state machine: a saga with a way back at every step stateDiagram-v2 [*] --> CREATED: idempotency key stored CREATED --> REJECTED: stock short CREATED --> RESERVED: stock held 5 min RESERVED --> CONFIRMED: paid or COD RESERVED --> CANCELLED: payment failed RESERVED --> PAYMENT_PENDING: gateway timeout RESERVED --> REFUNDED: paid after hold expired PAYMENT_PENDING --> CONFIRMED: reconciled paid PAYMENT_PENDING --> REFUNDED: paid, stock gone PAYMENT_PENDING --> CANCELLED: reconciled failed CONFIRMED --> PICKING: outbox event PICKING --> PACKED: short line substituted or refunded PACKED --> OUT_FOR_DELIVERY: rider scans bag OUT_FOR_DELIVERY --> DELIVERED OUT_FOR_DELIVERY --> RETURNED: customer unreachable DELIVERED --> [*] REJECTED --> [*] CANCELLED --> [*] REFUNDED --> [*] RETURNED --> [*] class DELIVERED ok class REJECTED,CANCELLED,REFUNDED,RETURNED bad class PAYMENT_PENDING queue
Decisions#
| Decision | Chose | Over | Because |
|---|---|---|---|
| When to reserve | At checkout | Add-to-cart, or at pick time | carts get abandoned; pick time is after the promise |
| Concurrency | Guarded UPDATE in one SQL transaction |
Read then write, locks, counter caches | tens a minute per store; the guard is atomic |
| Sharding | By store | By SKU | one basket = one local transaction, isolated stores |
| Store choice | H3 cell index + live mode, coverage first | Nearest store, polygon test per open | road distance, backlog and basket all matter |
| Picker queue | Earliest pack-by time | First come, first served | a far customer's longer ride makes it due sooner |
| Rider | Planned at confirmation; batch if detour ≤ 2 min | Assign after packing | waiting adds a minute or two to every order |
| The estimate | Per-order p90 from live state, modes with hysteresis | A fixed "10 minutes" | a slow night lands on the estimate, not the rider |
When it breaks#
| Failure | What users see | Contain it with |
|---|---|---|
| Phantom stock (bin empty) | substitute or a line refund | fix the count, recount, raise that SKU's buffer |
| Paid after the hold expired | automatic refund, clear message | PAYMENT_PENDING extends the hold; re-reserve or refund |
| Store loses power or network | new customers go to neighbours | heartbeats → CLOSED; cancel + refund if picking can't resume |
| Rain on match night: 3× demand, 30 % riders off | longer estimates, outer areas closed | ~110 orders/h vs ~576 wanted: shed outside in, cap hot SKUs |
| Customer unreachable | order returned, refund per policy | bounded wait, RETURNED, items rescanned |
| Wrong pin | told before paying | select on the address cell, re-check at checkout |
Say it in the interview#
- "The ten minutes are won in the store, not on the road." 2.5 + 8 = 10.5 min on averages; a true ten minutes is ~1.5 km, so the lever is store density.
- Two planes: reads are ~100× orders, so a seconds-stale in-stock bitmap serves browsing and checkout re-checks exactly.
- Reserve at checkout with a guarded update: the second buyer of the last packet fails the guard and hears "only 1 left" before paying.
- Shard by store: a basket is one local transaction, and one store's 9 PM rush stays in that store.
- The promise is a p90 prediction from live state, with modes and hysteresis; under load the outer cells close first.
- An unknown payment is not a failed one; and "order confirmed" leaves through an outbox so the store always hears about it.
- Plan the rider while the picker picks, and batch two orders only when nobody ends up late.