~learn.

ONDC, the whole design.

An open commerce network where buyer apps and seller apps run by different companies trade through one signed, asynchronous protocol (218 million transactions in FY 2025–26), kept consistent without a shared database by rules both sides follow: the buyer app names the order, the seller app holds it until its confirmation is acknowledged, and money moves only when both books match at a settlement agency.

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

SQ-32 · ONDC · design sheethimanshukukreja.in/learn/sheets/bonus-32-ondc-open-commerce

At a glance#

Number Value What it drives
Transactions, FY 2025–26 218M · ~7 a second on average orders are not the load; correctness is
Cumulative 500M on 25 July 2026 includes mobility, tickets, finance
Retail sellers live 1.16 lakh+ in 630+ cities (Dec 2025) catalogue size; search fan-out
Design target (assumed) ~200 orders / s · ~25 signed calls each ~5,000 signed calls / s network-wide
Live search fan-out (assumed) ~150 seller apps per domain and city cache catalogues; don't broadcast per query
Registry /lookup limit (recommended) 7,600 a minute (~127 / s) cache public keys by subscriber and key id
Settlement daily cycle two-sided /settle, matched and netted
Complaints 48 h to respond · 30 days to resolve IGM deadlines and escalation

Architecture#

DiagramParties talk directly; shared services stay thin
%% title: Parties talk directly; shared services stay thin
flowchart TD
  BUY@{ icon: "sd:users", label: "Buyers<br/>any app they already use" }
  BAP["Buyer apps · BAP<br/>catalogue cache · cart · collector"]:::core
  GW@{ icon: "sd:gateway", label: "Gateway<br/>multicasts search only" }
  REG@{ icon: "sd:id", label: "Registry<br/>subscribers · keys · domains" }
  BPP["Seller apps · BPP<br/>catalogues · quotes · orders"]:::hot
  IGM@{ icon: "sd:chat", label: "Issues · IGM<br/>48 h · 30 days · ODR" }
  LSP@{ icon: "sd:delivery", label: "Logistics apps<br/>bought by the seller app" }
  SOR@{ icon: "sd:store", label: "Sellers on record<br/>shops like Rehana's" }
  SA@{ icon: "sd:bank", label: "Settlement agency<br/>match · net · RTGS batch" }
  OBS@{ icon: "sd:dashboard", label: "Network observability<br/>masked logs · scorecards" }
  BUY ==> BAP
  BAP ==>|"search"| GW
  BAP ==>|"select · init · confirm"| BPP
  BAP --> IGM
  GW ==>|"by domain and city"| BPP
  GW -.->|"keys, cached"| REG
  BPP ==>|"cascaded logistics"| LSP
  BPP --> SOR
  BPP -->|"settle"| SA
  LSP -.->|"every NP pushes logs"| OBS
  class BUY client
  class GW,REG edge
  class IGM service
  class LSP external
  class SOR store
  class SA store
  class OBS service
DiagramHold, then release: one confirm at the seller app
%% title: Hold, then release: one confirm at the seller app
flowchart TD
  CONF["/confirm O-77<br/>network-unique order id"]:::service
  DUP{"Order id seen before?"}
  VAL{"Matches quote and terms?"}
  NACK["NACK 31002<br/>buyer app cancels 999"]:::bad
  HOLD@{ icon: "sd:queue", label: "Order HELD<br/>on_confirm in outbox" }
  ACKED{"Buyer app ACKs<br/>on_confirm in window?"}
  REL["Released to the shop<br/>Accepted"]:::core
  C998["Cancel 998<br/>push on_cancel"]:::bad
  CONF ==> DUP
  DUP -->|"yes: same answer"| HOLD
  DUP ==>|"no"| VAL
  VAL -->|"no"| NACK
  VAL ==>|"yes, ACK"| HOLD
  HOLD ==> ACKED
  ACKED ==>|"yes"| REL
  ACKED -->|"no"| C998
  class HOLD queue

Key flows#

DiagramThe Pune thali, from cached search to pickup
%% title: The Pune thali, from cached search to pickup
sequenceDiagram
  autonumber
  participant B as Buyer app
  participant S as Seller app
  participant L as Logistics app
  Note over B: search answered from its catalogue cache
  B->>S: select thali
  S-->>B: ACK
  S->>L: logistics search Moradabad to Pune
  L->>S: on_search · options
  S->>B: on_select · item + delivery charge
  B->>S: init · billing, address, payment terms
  S->>B: on_init · cancellation and settlement terms
  B->>S: confirm O-77 · buyer has paid
  S-->>B: ACK · order held
  S->>L: logistics confirm
  S->>B: on_confirm Accepted
  B-->>S: ACK · released to the shop
  L->>S: on_status Order-picked-up
  S->>B: on_status Order-picked-up
DiagramA silent confirm ends the same way on both sides
%% title: A silent confirm ends the same way on both sides
sequenceDiagram
  autonumber
  participant B as Buyer app
  participant S as Seller app
  B->>S: confirm O-77
  alt no ACK
    B->>S: confirm O-77 again · same id, capped retries
    Note over B: still nothing · cancel with 999, refund
  else ACK but no on_confirm reaches the buyer app
    S-->>B: ACK · order held, not sent to shop
    S->>B: on_confirm · retried from the outbox
    Note over S: no ACK in the window · cancel with 998, push on_cancel
    Note over B: no on_confirm in its window · cancel with 999, refund
  else lost ACK of on_confirm
    S->>B: on_confirm O-77 resent
    B-->>S: ACK again · same order, nothing new
  end
DiagramMoney moves only when both books match
%% title: Money moves only when both books match
sequenceDiagram
  autonumber
  participant C as Buyer app · collector
  participant R as Seller app · receiver
  participant A as Settlement agency
  C->>A: settle · inter-NP 1,000, collector 50, self 50
  R->>A: settle · inter-NP 1,000, collector 50, self 200, provider 800
  A->>A: dedupe, validate, match, balance check
  alt amounts match
    A->>C: on_settle SETTLED
    A->>R: on_settle SETTLED · net batch to RTGS, 800 to the seller
  else mismatch
    A->>R: on_settle NOT_SETTLED
    R->>C: recon · breakup
    C->>R: on_recon · agree or difference
    Note over C,R: agreed · settle next cycle · else an IGM issue
  end

Data model#

DiagramCore entities, each held by its own participant
%% title: Core entities, each held by its own participant
classDiagram
  class Participant {
    +subscriberId : string · domain name
    +role : buyer app, seller app, both
    +domains : list
    +cities : list
  }
  class SigningKey {
    +uniqueKeyId : string
    +publicKey : Ed25519
    +validUntil : time
  }
  class Order {
    +networkOrderId : string · set by the buyer app
    +transactionId : string
    +state : Created, Accepted, In-progress, Completed, Cancelled
    +quote : items + delivery + fees
  }
  class Fulfillment {
    +id : string
    +type : delivery, return, RTO
    +state : Pending to Order-delivered
  }
  class LogisticsOrder {
    +id : string
    +provider : string
    +pickupCode : string
    +deliveryCode : string
  }
  class SettlementLine {
    +orderId : string
    +interParticipant : amount
    +collector : amount
    +self : amount
    +provider : amount
    +status : SETTLED or NOT_SETTLED
  }
  class Issue {
    +id : string
    +type : issue, grievance, dispute
    +actions : append-only list
    +expectedResolution : time
  }
  Participant "1" --> "many" SigningKey : publishes
  Order "1" --> "many" Fulfillment : tracks
  Fulfillment "1" --> "0..1" LogisticsOrder : bought as
  Order "1" --> "many" SettlementLine : settled by
  Order "1" --> "many" Issue : complained about in

Lifecycle#

DiagramAn order on both sides, or on neither
%% title: An order on both sides, or on neither
stateDiagram-v2
  [*] --> CONFIRM_SENT: buyer app names the order
  CONFIRM_SENT --> HELD_AT_SELLER: seller ACKs
  CONFIRM_SENT --> CANCELLED_999: NACK or silence after retries
  HELD_AT_SELLER --> ACCEPTED: on_confirm ACKed, released
  HELD_AT_SELLER --> CANCELLED_998: on_confirm NACKed or unanswered
  ACCEPTED --> IN_PROGRESS: packed, picked up
  IN_PROGRESS --> COMPLETED: delivered
  ACCEPTED --> CANCELLED: cancel or force cancel
  IN_PROGRESS --> CANCELLED: RTO initiated
  COMPLETED --> [*]
  CANCELLED_999 --> [*]
  CANCELLED_998 --> [*]
  CANCELLED --> [*]
  class COMPLETED ok
  class CANCELLED_999 bad
  class CANCELLED_998 bad
  class HELD_AT_SELLER queue

Decisions#

Decision Chose Over Because
Discovery Cached catalogues, full + incremental refresh Live broadcast per search fan-out per query waits for the slowest of ~150
Order identity Buyer app names it, network-unique Seller app assigns ids retries collide on one id
Release to shop After on_confirm is ACKed On /confirm the shop never packs an order the buyer app lacks
Failure outcome Fixed codes, 999 and 998 Ad hoc retries both sides reach "cancelled" without talking
Courier cancels Re-procure, then cascade cancel Cancel the order at once most orders survive one courier
Settlement Two-sided /settle, agency matches Collector pays on its own say neither side trusts the other's books
Settlement timing After terminal state + return window At delivery refunds become reductions, not clawbacks

When it breaks#

Failure What users see Contain it with
Seller app down at peak its items unavailable per-counterparty breaker; keep cached catalogue; 999 refunds
ACKs confirm, never sends on_confirm "confirming…", then refund hold-then-release; outbox-age alerts; network attempt-to-confirm rate
Signing key leaks nothing, if revoked fast new key id; short key-cache TTL; refresh on failure
Clock skew callbacks rejected as stale NTP; ±10 s rule; skew metric per counterparty
Amounts disagree seller paid late NOT_SETTLED; recon and on_recon; IGM if unresolved
Collector account short seller paid late balance check fails; orders wait until funded

Say it in the interview#