~learn.

Blinkit Quick Commerce, the whole design.

Gets a basket from a dark store ~2 km away to the door in about ten minutes, a few million times a day across ~2,500 stores, by winning the minutes inside the store and showing each customer an estimate the store can actually keep.

7 diagrams · 8 sections · revise in ~10 min · Download PDF prints it in the light theme, diagrams included

SQ-12 · Blinkit Quick Commerce · design sheethimanshukukreja.in/learn/sheets/bonus-12-blinkit-quick-commerce

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
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 cache

Key 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
  end
DiagramEmpty 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 buffer

Data 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 from

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#