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 Run One Order Through Four Companies That Don't Trust Each Other's Databases?
It is Dhanteras evening in Moradabad, the night families buy brass and silver for luck. Rehana Ansari, 41, runs Ansari Brass Works from a narrow shop near the old market: puja thalis, diyas, small idols, polished by her brother in the back room. Two years ago she listed her stock on a seller app her trade association recommended. She has never built a website, and she has never heard of most of the apps her orders come from.
At 7:41 pm her tablet chimes: a puja thali, ordered from Pune through a payments app. At 7:43, diyas for a family in Kolkata, through a bank's shopping tab. At 7:44, two people tap "Add to cart" on her last brass Lakshmi–Ganesh set within a second: one in Lucknow on a grocery app, one in Jaipur on a loyalty marketplace. Rehana sees one order for it; the Jaipur buyer sees "Sorry, this item is no longer available".
The Pune buyer has a worse minute. She pays, the screen says "Confirming your order with the seller…", and the spinner keeps spinning. She taps "Place order" again. Two minutes later: "Order placed". Rehana's tablet shows one thali order from Pune, not two. At 8:10 a rider from a small Moradabad delivery company reads out a pickup code and takes the parcels. None of these companies runs the others. They agree on nothing except a protocol, a registry of who's who, and rules for money and complaints.
- 29 April 2022: the first order on the network, "a handful of dhaniya, delivered in Bengaluru"; 1 million+ transactions by May 2023, 10 million+ by August 2023, 100 million+ by August 2024, 250 million+ by June 2025 and 500 million on 25 July 2026 (ONDC, "Our journey")
- 218 million transactions in FY 2025–26 (ONDC, FY 2025–26 in Focus, 2026)
- 15.4 million+ orders in December 2024, about 4.9 lakh a day on average; 154.4 million+ cumulative by then; 7.64 lakh+ sellers and service providers registered across 616+ cities, as of January 2025 (PIB factsheet, February 2025)
- 1.16 lakh+ retail sellers live from 630+ cities and towns, as of 9 December 2025 (Minister of State for Commerce and Industry, Lok Sabha written reply, PIB, 16 December 2025)
- ~1.2 million transactions in the Diwali week of 6–13 November 2023, from 600+ cities (Business Today, 17 November 2023)
- 50,000+ delivery executives across 250+ cities; regional and small fleet operators carry 35% of fulfilled logistics orders (ONDC, FY 2025–26); 50+ logistics partners live by June 2026 (ONDC)
For context:
A "transaction" on ONDC is not only a retail order: it includes ride-hailing, metro and bus tickets and financial products. In October 2024, for example, Inc42, citing a Moneycontrol report based on an unnamed industry source, put the month at 14 million transactions, 5.5 million of them mobility. ONDC's FY 2025–26 newsletter reports "over 300,000 tickets per day" across metro and bus services
Rehana, her shop and her orders are illustrative; the buyer apps in her story are described, not named
ONDC publishes its protocol, its API contracts, its settlement and grievance frameworks and its onboarding process openly on GitHub and its website. Those are the published parts. How each participant builds its own app is not published, and everything labelled as our design or an assumption is illustrative
This is the system we'll design today.
ONDC reached 500 million transactions on 25 July 2026, with 218 million in FY 2025–26 (rides, tickets and financial products included) and 1.16 lakh+ retail sellers live in 630+ cities and towns. That is only ~7 transactions a second on average; on stated assumptions an order takes ~25 signed calls, and the real load is search, a broadcast to ~150 seller apps per city. A gateway multicasts searches only; select, init, confirm and everything after go peer to peer between buyer and seller apps as an ACK now and an on_ callback later, every message signed with Ed25519 over a BLAKE-512 digest and verified against the registry. Buyer apps serve search from catalogue caches fed by full refreshes and 1–30 minute pushes. The buyer app names each order with a network-unique id; the seller app holds it and releases it to the shop only when its on_confirm is acknowledged, and silence ends in a 999 or 998 cancellation. The seller app buys delivery on the same network and cascades its states, and a settlement agency moves money only when both sides' /settle instructions match: in ONDC's example a ₹1,050 order sends ₹800 to the seller. The edge cases are a festival evening, a complaint routed over IGM and a seller app that ACKs confirms but never answers.
The Interview Begins#
You're interviewing at a company that wants to join an open commerce network as a seller-side platform. The interviewer shares two screenshots. The first is a seller's order list on a tablet: four orders in four minutes, each tagged with a different buyer app's name. The second is a buyer's phone showing "Confirming your order with the seller…" under a spinner.
"These four orders came through four different companies' apps, into a fifth company's app, and they'll be delivered by a sixth. Nobody in this picture shares a database with anyone else. Design the network: how a buyer on any app finds this seller, how an order comes to exist on both sides, how delivery is bought, how the money moves, and what happens when something breaks."
Buyer-side apps (any company with users: a payments app, a bank, a grocery app) and seller-side apps (platforms that onboard sellers and hold their catalogues) are run by different companies. Any buyer app must be able to discover any seller on any seller app and place an order. Delivery is bought by the seller app from logistics providers on the same network. Payment is collected by one party and settled to the others. Complaints raised on the buyer's app must reach whoever can fix them. A network operator runs only a small set of shared services: a registry of participants, gateways for discovery, and the rules.
Requirements
- One protocol for search, select, init, confirm, status, track, update, cancel and return, between any two participants
- Discovery across all seller apps for a domain and city, without every search hitting every seller app
- Every message authenticated and tamper-evident, between parties that have never met
- An order that both sides agree exists, with the same items, price and state, through retries and failures
- Logistics as another participant, with delivery status flowing back to the buyer
- Settlement and reconciliation of each order's money between collector, receiver and seller, and refunds
- Grievances that cross company boundaries, and a view of the network's health that leaks nobody's data
"Payments themselves are UPI, cards and wallets; assume they exist. The UPI problem is a separate one. I want the network: the protocol, the trust, the consistency across companies, and the money after the payment."
People often call ONDC "UPI for commerce", and the analogy was there from the start: Nandan Nilekani, an adviser to ONDC, wanted to do for e-commerce what UPI had done for payments (Bloomberg, April 2022). The analogy is useful for the shape (many apps on one set of open rails, with a neutral operator in the middle) and misleading for the mechanics. A UPI payment is one atomic debit and credit routed through a central switch; we designed that in UPI (Bonus Problem 1). An ONDC order is a conversation between companies that lasts from a search to a return window days later, with no central switch in the middle of it at all.
Phase 1: Requirements Clarification (5 minutes)#
"Before I draw anything I want to know what's central and what isn't, because that decides where consistency can come from."
Your Questions#
"Does the network operator see every order?"
"No. ONDC runs a registry of participants and gateways for search. ONDC's own FAQ says only discovery messages go through a gateway; after that, /select, /init, /confirm and the rest go directly between the buyer app and the seller app. The gateway multicasts searches and 'does not send responses back': seller apps reply directly to the buyer app."
"So there's no shared order database anywhere. How does a seller app know a request really came from the buyer app it claims?"
"Right, none. Every request and callback is signed with the sender's Ed25519 key over a BLAKE-512 digest of the body, and the receiver looks up the sender's public key in the registry. ONDC's signing document treats each request/callback pair as a 'contract' between two parties."
"How big is it?"
"218 million transactions in FY 2025–26, across retail, mobility, transit tickets and financial services; the government put December 2024 at 15.4 million orders, about 4.9 lakh a day. On the retail side, 1.16 lakh+ sellers were live in 630+ cities and towns in December 2025. The Diwali week of 2023 saw about 1.2 million transactions; assume festival evenings are your peak."
"Who collects the buyer's money?"
"It's negotiated per order: the buyer app or the seller app can collect, and for cash on delivery the logistics agent does. In retail the buyer app usually collects and settles with the seller app through a settlement agency, under ONDC's Reconciliation and Settlement Framework."
Functional Requirements#
- Onboarding: a participant registers its domain name, roles, domains, cities and public keys with the registry, proves it controls the domain, and is certified by its logs before going live
- Discovery: a buyer app's search reaches every seller app registered for that domain and city; seller apps answer with catalogues directly to the buyer app; catalogues can be pulled in full and refreshed incrementally
- Ordering: select (cart and quote, including delivery charge), init (billing, fulfilment and payment terms), confirm (the order), all as request and callback pairs
- Post-order: status, track, update (part cancel, return), cancel; the seller app pushes every state change unsolicited
- Logistics: the seller app buys delivery from logistics providers through the same protocol, and their states flow back into the retail order
- Money: collector and receiver submit settlement instructions to a settlement agency; mismatches are reconciled between them; refunds and disputes have a path
- Complaints: an issue raised on the buyer app is routed to the seller app and, if needed, the logistics provider, with response and resolution deadlines
Non-Functional Requirements#
Trust
- Every message signed by its sender and verified by its receiver; keys rotate without downtime
- No participant can read another participant's data except what the transaction needs
Correctness across companies
- An order exists on both sides with the same id, items, quote and state, or on neither
- Every request and callback is idempotent; retries never create a second order, charge or settlement
- Money for each order matches on both sides before it moves
Resilience and latency (design targets)
- A slow or broken participant affects only its own transactions
- Callbacks within the message's
ttl(the contract's examples usePT30S); search results usable within ~2 s from cached catalogues - Settlement daily; complaints resolved within the legal limit of 30 days
Phase 2: Back of the Envelope Estimation (5 minutes)#
"My anchors are published: ONDC's 218 million transactions in FY 2025–26, the government's 4.9 lakh orders a day in December 2024 and 1.16 lakh retail sellers live, and ONDC's recommended registry rate limits. ONDC doesn't publish per-participant traffic, seller-app counts per city, catalogue sizes or message counts, so those are assumptions, and I'll label them."
Before the numbers: how busy is the network, really?
218 million transactions in a year: how many is that a second, on average? Suppose a festival evening runs at 10× the average. Then count the messages: a retail order needs select, init and confirm, each with a callback, plus about five status updates, and the seller app runs its own search, init and confirm with a logistics provider. Roughly how many signed HTTP calls does one retail order take? Where, then, is the real load?
One strong answer
- Orders: 218 million ÷ 365 ≈ 600,000 a day ≈ 7 a second on average. A 10× festival evening is ~70 a second across the whole network. Tiny by the standards of a single big marketplace.
- Messages per order (assumed): retail select, init, confirm and their callbacks (6) + ~5 unsolicited status callbacks + logistics search with ~4 replies, init, confirm and their callbacks (~9) + ~5 logistics status callbacks ≈ ~25 signed calls, each answered first by an ACK.
- So a 70-orders-a-second evening is ~1,750 signed calls a second spread over hundreds of participants. Orders aren't the load.
- The load is search and catalogues: a single live search is multicast to every seller app in that domain and city. That fan-out, not ordering, is what the design has to tame.
Orders and Messages#
| Value | |
|---|---|
| Transactions, FY 2025–26 (ONDC) | 218 million |
| Average a day / a second (our arithmetic) | ~600,000 / ~7 |
| December 2024 orders, average a day (PIB) | ~4.9 lakh |
| Festival-evening peak (assumed 10× average) | ~70 a second |
| Design target (assumed 3× that peak) | ~200 orders a second |
| Signed calls per retail order, incl. logistics (assumed) | ~25, each with an ACK |
| Signed calls at the design target | 200 × 25 ≈ ~5,000 a second, network-wide |
Conclusion: the ordering path is small in volume. What makes it hard is that every one of those calls crosses a company boundary, can be lost, retried or arrive late, and has to leave both sides agreeing.
Search Fan-Out and Catalogues#
- Seller apps per domain and city: not published; assume ~150 for a large city's grocery domain
- Live search: each buyer search goes through a gateway to all ~150 and comes back as up to ~150
on_searchcallbacks. At an assumed 1,000 searches a second on a festival evening, that's 150,000 requests a second into seller apps, and every search waits for the slowest - Catalogue size: 1.16 lakh retail sellers × an assumed ~300 items ≈ 35 million items; at ~3 KB of JSON each ≈ ~100 GB
- Caching instead: pull each city's catalogue in full once a day, then take changes: if ~2% of items change a day (assumed), that's ~700,000 item updates a day per caching buyer app
Conclusion: search is a broadcast, and broadcasts don't scale per query. Pay the broadcast once per city per refresh, cache the catalogue at the buyer app, and serve searches from the cache. That is what ONDC's retail contract does.
Keys, Registry and Money#
- Registry lookups: ONDC recommends a limit of 7,600 a minute for
/lookup(~127 a second). A seller app taking 1,000 signed calls a second must cache public keys by(subscriber_id, unique_key_id) - Signature checks: an Ed25519 verification costs tens of microseconds on one core (a general figure); crypto isn't the bottleneck, key distribution is
- Settlement: one daily cycle. If ~half of ~600,000 daily transactions settle through the framework (assumed), the settlement agency matches ~600,000 order lines a day, one from each side per order, then nets them into one batch
Conclusion: trust is cheap per message unless every message calls the registry; money is a daily batch whose difficulty is matching two companies' books, not throughput.
Key Metrics Summary#
| Metric | Value |
|---|---|
| Transactions in FY 2025–26 (ONDC) | 218 million, ~7 a second on average |
| Orders in December 2024 (PIB) | 15.4 million+, ~4.9 lakh a day |
| Retail sellers live (Lok Sabha reply, December 2025) | 1.16 lakh+, 630+ cities and towns |
| Design target for orders (assumed) | ~200 a second, ~25 signed calls each |
| Live search fan-out (assumed) | ~150 seller apps per domain and city |
Registry /lookup limit, recommended (ONDC) |
7,600 a minute |
| Settlement cycle (ONDC RSF) | daily |
Requirements and estimation
Q1/3 ONDC reported 218 million transactions in FY 2025–26. Roughly how many is that a second on average?
Answer & explanation
Answer: B
218 million ÷ 365 ÷ 86,400 ≈ 7 a second. Even a 10× festival evening is ~70 a second.
Q2/3 Why can't every buyer's search be a live broadcast to all seller apps?
Answer & explanation
Answer: B
Broadcast cost grows with searches × seller apps. Paying it once per catalogue refresh and serving searches from a cache removes it from the query path.
Q3/3 The registry's recommended /lookup limit is 7,600 a minute. What follows for a busy seller app?
Answer & explanation
Answer: C
~127 lookups a second can't cover thousands of verifications a second; a key cache keyed by subscriber and key id can.
Phase 3: High-Level Design (10 minutes)#
Sketch it first: the Pune thali, from search to rider
The buyer in Pune searches "brass puja thali" in her payments app. Where do the results come from, and did her search reach Rehana's seller app at that moment? She adds the thali to her cart: who quotes the delivery charge, and who did they ask? She pays: which company holds her money, and who tells Rehana's seller app it's paid? Her app sends "confirm" and hears nothing for 30 seconds: what must not happen? And at 8:10 the rider arrives: whose order is he fulfilling?
One strong answer
- Results come from the payments app's own catalogue cache, filled earlier by catalogue refreshes from Rehana's seller app. Her search didn't touch Rehana's seller app.
- The quote:
/selectgoes straight to Rehana's seller app, which checks live stock and, acting as a logistics buyer, asks logistics providers for a Moradabad–Pune delivery./on_selectreturns item price plus delivery charge. - The money: the payments app collects it and says so in
/confirm; it reaches Rehana days later, through the settlement agency. - The silent confirm: the retry must not create a second order. The buyer app created a network-unique order id and resends the same
/confirm. - The rider fulfils a logistics order that Rehana's seller app placed, linked to the retail order; his updates are cascaded to the buyer app.
"Four kinds of party (buyer apps, seller apps, logistics apps, the settlement agency) and a few thin shared services (registry, gateways, grievance and observability). Everything that matters in an order happens on edges between two parties, signed at one end and verified at the other. No box in the middle knows the order."
System Architecture#
%% title: An open network: parties talk directly; the shared services stay thin
flowchart TD
subgraph L1["Layer 1 · Buyers"]
direction LR
BUY@{ icon: "sd:users", label: "Buyers<br/>Pune, Kolkata, Lucknow, Jaipur" }
BAP["Buyer apps · BAP<br/>catalogue cache, cart, checkout, collector"]:::core
end
subgraph L2["Layer 2 · Shared network services"]
direction LR
REG@{ icon: "sd:id", label: "Registry<br/>subscribers, keys, domains, cities" }
GW@{ icon: "sd:gateway", label: "Gateway<br/>multicasts search only" }
end
subgraph L3["Layer 3 · Supply"]
direction LR
BPP["Seller apps · BPP<br/>catalogues, quotes, orders"]:::hot
SOR@{ icon: "sd:store", label: "Sellers on record<br/>Ansari Brass Works" }
LSP@{ icon: "sd:delivery", label: "Logistics apps<br/>couriers, small fleets" }
end
subgraph L4["Layer 4 · Money and trust"]
direction LR
SA@{ icon: "sd:bank", label: "Settlement agency<br/>match, net, bank instructions" }
IGM@{ icon: "sd:chat", label: "Issues and grievances<br/>IGM, GROs, ODR" }
OBS@{ icon: "sd:dashboard", label: "Network observability<br/>masked logs, scorecards" }
end
BUY ==> BAP
BAP ==>|"search"| GW
GW ==>|"multicast by domain, city"| BPP
BPP -.->|"on_search, direct"| BAP
BAP ==>|"select, init, confirm, status"| BPP
BPP ==>|"logistics search to confirm"| LSP
BPP --> SOR
BAP -.->|"key lookup, cached"| REG
BPP -.->|"key lookup, cached"| REG
BAP -->|"settle"| SA
BPP -->|"settle"| SA
BAP --> IGM
BPP -.->|"logs"| OBS
class BUY client
class REG,GW edge
class SOR store
class LSP external
class SA store
class IGM service
class OBS serviceKEY INSIGHTS:
- The network is a protocol, not a platform. The registry and gateways carry identities and searches; orders, quotes, statuses and money instructions go peer to peer
- Consistency comes from the protocol's rules, not from a database. Ids, idempotent request and callback pairs, ordered states and deterministic failure rules make two sets of books agree
- Every edge is a trust boundary. Each call is signed by its sender and verified against the registry by its receiver, so a participant can be held to what it sent
Data Flow: The Pune Thali#
%% title: The Pune thali, from cached search to rider pickup sequenceDiagram autonumber participant B as Payments app · BAP participant S as Rehana's seller app · BPP participant L as Logistics app Note over B: search served from its catalogue cache B->>S: select thali, txn T1 S-->>B: ACK S->>L: logistics search Moradabad to Pune L-->>S: ACK, then on_search with options S->>B: on_select quote · item + delivery B->>S: init billing, address, payment terms S->>L: logistics init S->>B: on_init with cancellation and settlement terms B->>B: buyer pays · order id O-77 created B->>S: confirm O-77 S-->>B: ACK · order held, not yet sent to shop S->>L: logistics confirm S->>B: on_confirm O-77 Accepted B-->>S: ACK · now released to Rehana L->>S: on_status Order-picked-up S->>B: on_status Order-picked-up
MEANWHILE, IN PARALLEL:
- Every arrow is signed by its sender and verified against a cached copy of the sender's registry key
- One
transaction_idruns through select, init and confirm; each request and callback pair has its ownmessage_id, so either side can match callbacks and drop duplicates - Both sides log every message, and push masked copies to network observability
High-level design
Q1/3 Which messages in a retail order pass through a gateway?
Answer & explanation
Answer: A
ONDC's FAQ: only discovery-phase messages go through the gateway, and it doesn't relay responses.
Q2/3 In the Pune flow, who decides the delivery charge in on_select?
Answer & explanation
Answer: C
Logistics is another network transaction, run by the seller app inside the retail one.
Q3/3 What does an ACK to /confirm mean?
Answer & explanation
Answer: B
Beckn calls are asynchronous: the synchronous reply is only an acknowledgement.
Phase 4: Deep Dives (20 minutes)#
Deep Dive 1: Every Message Is a Signed Contract (Week 9 Concepts: Security Architecture; Week 2: Webhook Delivery)#
"In a single company, a request from the cart service to the order service is trusted because both run inside the same network. Here the 'cart service' is a payments company and the 'order service' is a seller platform in another city. They've never exchanged a credential. The registry is how they meet, and the signature is how each message proves where it came from."
This builds on Week 9, Day 5 · Security Architecture and Week 2, Day 4 · Webhook Delivery.
A request from a stranger
Rehana's seller app receives a /select that claims to be from the Jaipur loyalty marketplace. It has never talked to that company before. How does it know the request came from that company and wasn't changed on the way? Where does it get the right public key, and what if it gets 1,000 such requests a second? The same request arrives a second time, 40 seconds later, byte for byte identical: is that an attack? And the marketplace rotates its key tonight: how do in-flight messages survive?
One strong answer
- Signature: the sender hashes the exact body with BLAKE-512 (BLAKE2b), builds a signing string from
(created),(expires)and that digest, signs it with its Ed25519 private key, and sends it in anAuthorizationheader withkeyId="subscriber_id|unique_key_id|ed25519". The receiver recomputes the digest from the raw bytes it received and verifies the signature. - The key: looked up in the registry by subscriber id and key id, then cached. At 1,000 a second the cache answers almost all of them; the registry's recommended
/lookuplimit is ~127 a second. - A replay: the signature has an expiry, and the message's
context.timestampmust be near the receiver's clock (ONDC's FAQ says within ±10 seconds). A 40-second-old copy of the samemessage_idis either a retry (handled idempotently) or stale and NACKed. - Rotation: the registry can hold more than one key per subscriber, each with its own
unique_key_id. Publish the new key, start signing with it, and keep the old one valid until messages signed with it have expired.
What is published: ONDC's signing document says that because of "the commercial nature of the transactions, every request/callback pair is considered to be a 'contract' between two parties", so all requests and callbacks are signed and verified. The digest is BLAKE-512 over the body; the signing string is (created), (expires) and the digest; the signature is Ed25519, in an Authorization header whose keyId is {subscriber_id}|{unique_key_id}|{algorithm}. A gateway forwarding a search adds its own X-Gateway-Authorization header, and the seller app verifies both. With no valid key, the receiver NACKs as unauthorised. To join, a participant needs a domain name (its subscriber_id) with an SSL certificate the registry checks by OCSP, an Ed25519 signing key pair and an X25519 encryption key pair, a site-verification page carrying a signed request id, and an /on_subscribe endpoint that must decrypt the registry's encrypted challenge. The registry's recommended rate limits are 10 a minute for /subscribe and 7,600 for /lookup.
%% title: Joining the network, then every message after
flowchart TD
NP["New participant<br/>FQDN, SSL, two key pairs"]:::service
SUB@{ icon: "sd:id", label: "Registry /subscribe<br/>schema, OCSP, site check" }
CH@{ icon: "sd:lock", label: "Challenge to /on_subscribe<br/>X25519 shared key, AES" }
LIVE["Listed: roles, domains,<br/>cities, public keys"]:::core
SIGN["Each request: BLAKE-512 digest,<br/>Ed25519 signature, keyId"]:::hot
RX["Receiver: key from cache<br/>or /lookup, then verify"]:::core
BAD["NACK, unauthorised"]:::bad
NP ==> SUB ==> CH ==> LIVE
LIVE -.->|"keys published"| RX
SIGN ==> RX
RX -.->|"no key or bad signature"| BAD
class SUB edge
class CH edge- Sign the bytes you send, verify the bytes you got. Re-serialising JSON changes whitespace and key order; ONDC's auth-header guide says to hash the "raw payload".
- Cache keys, not answers. A cache keyed by
(subscriber_id, unique_key_id)with a TTL of minutes absorbs nearly every verification. On a failure, refresh that one key once before NACKing: it may have just rotated. - The registry is a dependency with a budget. If it's slow, cached keys keep the network running; only new participants wait, which is the right failure.
- Signatures make logs evidence. A signed
/on_selectwith a price is provably what the seller app said; that matters in disputes and reconciliation. - It's webhook delivery in both directions. Every callback is a webhook between companies: signed, retried with a cap, deduplicated by id.
# network/auth.py
"""
Signing and verifying ONDC-style requests (illustrative, using PyNaCl and hashlib).
Digest: BLAKE2b-512 of the raw body. Signing string: (created), (expires), digest.
Keys are looked up in the registry by subscriber id and key id, then cached.
"""
import base64, hashlib, time
from nacl.signing import SigningKey, VerifyKey
def signing_string(body: bytes, created: int, expires: int) -> bytes:
digest = base64.b64encode(hashlib.blake2b(body, digest_size=64).digest()).decode()
return f"(created): {created}\n(expires): {expires}\ndigest: BLAKE-512={digest}".encode()
def sign(body: bytes, sk: SigningKey, subscriber_id: str, key_id: str, ttl_s: int = 300) -> str:
created = int(time.time()); expires = created + ttl_s
sig = base64.b64encode(sk.sign(signing_string(body, created, expires)).signature).decode()
return (f'Signature keyId="{subscriber_id}|{key_id}|ed25519",algorithm="ed25519",'
f'created="{created}",expires="{expires}",headers="(created) (expires) digest",'
f'signature="{sig}"')
def verify(raw_body: bytes, header: dict, keys) -> bool:
sub, key_id, alg = header["keyId"].split("|")
if alg != header["algorithm"] or not (int(header["created"]) <= time.time() <= int(header["expires"])):
return False
msg = signing_string(raw_body, int(header["created"]), int(header["expires"]))
for attempt in (0, 1):
pub = keys.get(sub, key_id, refresh=(attempt == 1)) # cache first, registry on retry
if pub is None:
continue
try:
VerifyKey(base64.b64decode(pub)).verify(msg, base64.b64decode(header["signature"]))
return True
except Exception:
continue # maybe rotated: refresh once
return False # caller NACKs: unauthorisedDeep Dive 2: Discovery: a Broadcast You Pay Once (Week 7 Concepts: Indexing Pipeline; Week 2: Timeout Hell; Week 4: Cache Invalidation)#
"A marketplace searches its own index. A buyer app on an open network has no index of its own until it builds one from what every seller app tells it. The question is whether that index is built per query, by broadcasting each search, or ahead of time, by broadcasting a request for catalogues and caching the answers."
This builds on Week 7, Day 2 · Indexing Pipeline, Week 2, Day 1 · Timeout Hell and Week 4, Day 2 · Cache Invalidation.
The last Lakshmi–Ganesh set
At 7:44 Rehana has one brass Lakshmi–Ganesh set left. The Lucknow grocery app and the Jaipur marketplace both show it "in stock" from their caches. Both buyers tap "Add to cart" within a second. What does each buyer app send, and who decides who gets it? Earlier that evening, a live search on a third buyer app went to 150 seller apps through the gateway and 30 hadn't answered after 3 seconds: what should it show? And Rehana's seller app was down for 20 minutes at 6 pm and missed a full catalogue request: are her products now "gone" from that buyer app?
One strong answer
- The cart: both send
/selectdirectly to Rehana's seller app, which checks its live stock. One gets a quote; the other gets an/on_selectsaying the item isn't available. The cache was a hint;/selectis where truth is checked. In our design the seller app doesn't reserve stock at select, so it rechecks at/initand/confirm. - Partial results: show what has arrived, ranked, and keep accepting late
on_searchcallbacks until the request'sttlexpires, inserting them if the buyer is still on the page. Never wait for all 150. - Missed refresh: no. A full refresh overwrites a provider's items only when that provider's response arrives; a provider that didn't answer keeps its cached items. ONDC's contract says providers must be disabled explicitly, through an incremental refresh, and "cannot be disabled if a response isn't received".
What is published: ONDC's retail API contract (1.2.0) says search "will be used to communicate the intent for a full or incremental catalog refresh" and is "always broadcast through the gateway"; ONDC's FAQ adds that the gateway filters seller apps by domain and city. The full catalogue "will be sent directly by the seller NPs to the buyer NP", by city (for grocery, optionally by sub-category), inline or as a time-limited download link; the contract notes that "most buyer apps cache catalogs". For incremental refresh, a buyer app can pull a time window or ask for pushes "at a frequency within a range of 1-30 mins"; changes carry timestamps at provider, location and item level; and a seller app "should push catalog updates… until ACK is received from all buyer NPs". The search intent also declares the buyer app's finder fee (the example is 3%). In December 2025 the Commerce Ministry told the Lok Sabha that seller-side apps make their full catalogues discoverable to all buyer-side apps, while buyer-side apps disclose "key parameters used for sorting or listing search results".
%% title: Two ways to search an open network
flowchart TD
Q@{ icon: "sd:search", label: "Buyer types<br/>brass puja thali" }
MODE{"Catalogue cached<br/>for this city?"}
IDX["Buyer app's own index<br/>answers in milliseconds"]:::core
BC@{ icon: "sd:gateway", label: "Live search via gateway<br/>to every seller app" }
WAIT["Collect on_search until ttl<br/>show partial results"]:::hot
REF["Full refresh daily by city,<br/>incremental pushes 1 to 30 min"]:::service
Q ==> MODE
MODE ==>|"yes"| IDX
MODE -->|"no, or new category"| BC ==> WAIT
REF -.->|"keeps it fresh"| IDX
class Q client- The buyer app owns an indexing pipeline. Catalogue refreshes are its ingestion: Week 7's pipeline, except the documents arrive as signed callbacks from a hundred companies.
- Order by the seller's timestamps, not by arrival. A full refresh ingested from 8:00 to 9:00 can race with pushes from 8:30. Keep the latest event timestamp per item; ignore anything older.
- Absence isn't deletion. Disabling a provider, a store or an item is an explicit message.
- Stock in the cache is a hint. The seller app's
/on_selectis authoritative, and it pushes stock-outs in its next incremental refresh. - Live search is for gaps. A new city, an uncached category, a ride: broadcast, collect until
ttl, show partial results. The slow seller app loses its slot, not the buyer's patience. - The seller app runs pub–sub too. With 25 subscribed buyer apps, it aggregates changes once and fans them out 25 times, with ACKs and retries.
# buyer_app/catalog_ingest.py
"""
Ingesting full and incremental catalogue refreshes (illustrative). Each item update
carries the seller's event timestamp; the cache keeps the latest per item, so a slow
full refresh can't overwrite newer incremental changes. Providers are disabled only
by an explicit message, never because a response didn't arrive.
"""
def apply_full_refresh(cache, bpp_id: str, provider: dict, as_of: str) -> None:
pid = (bpp_id, provider["id"])
seen = set()
for item in provider["items"]:
key = pid + (item["id"],)
seen.add(key)
if cache.item_ts(key) is None or cache.item_ts(key) <= as_of:
cache.put_item(key, item, ts=as_of)
for key in cache.items_of(pid): # items this provider no longer lists
if key not in seen and cache.item_ts(key) <= as_of:
cache.disable_item(key, ts=as_of) # only for providers that DID respond
def apply_incremental(cache, bpp_id: str, provider: dict) -> None:
pid = (bpp_id, provider["id"])
if provider.get("disabled"): # explicit: the only way a provider goes away
cache.disable_provider(pid, ts=provider["time"]["timestamp"])
return
for item in provider.get("items", []):
key, ts = pid + (item["id"],), item["time"]["timestamp"]
if cache.item_ts(key) is None or ts > cache.item_ts(key):
cache.put_item(key, item, ts=ts) # newer wins; older pushes are ignoredSigning and discovery
Q1/3 A receiver parses the JSON body, re-serialises it, and then verifies the digest. What goes wrong?
Answer & explanation
Answer: B
Sign the bytes you send; verify the bytes you got.
Q2/3 Two buyer apps both show the last item in stock and both send /select. Who decides?
Answer & explanation
Answer: C
Cached stock is a hint; the seller app is the source of truth for its own inventory.
Q3/3 A live search has 120 of 150 replies after 3 seconds. What do you show?
Answer & explanation
Answer: B
Partial results are the normal case in a broadcast; the slowest seller app shouldn't set the buyer's latency.
Deep Dive 3: One Order, Two Databases (Week 2 Concepts: Idempotency; Week 5: Saga Pattern; Week 3: Transactional Outbox)#
"This is the heart of it. The buyer app has an order row; the seller app has an order row; there is no transaction that spans both. They agree only if every message about the order is idempotent, every failure has a rule both sides follow, and the order isn't released to the shop until both sides have heard each other. It's a saga between two companies, with the protocol as the coordinator."
This builds on Week 2, Day 2 · Idempotency in Practice, Week 5, Day 2 · Saga Pattern and Week 3, Day 2 · Transactional Outbox.
The spinner in Pune
The Pune buyer has paid. Her payments app sends /confirm for order O-77. Three things can go wrong: Rehana's seller app never ACKs; it ACKs but /on_confirm never arrives; or /on_confirm arrives but the payments app's ACK of it is lost, so the seller app retries /on_confirm. For each, what does each side do so that they end with the same answer? When may Rehana's tablet show the order? And the buyer taps "Place order" a second time: what id does the second /confirm carry?
One strong answer
- The id: the buyer app creates a network-unique order id before the first
/confirm; every retry, and the second tap, carries it./confirmis idempotent. - No ACK: the buyer app retries a capped number of times, then cancels with reason code 999, "order confirmation failure", and refunds.
- ACK, no
/on_confirm: the seller app holds the order and retries/on_confirm; with no ACK in its window, it cancels with 998 and pushes the cancellation. The buyer app keeps retrying/confirmwith the same id and, with no/on_confirmin its own window, cancels with 999 and refunds. Status asks meanwhile get "processing" (31003). - Lost ACK of
/on_confirm: the seller app resends the same callback; the buyer app recognises the order and ACKs again. - Rehana sees it only after the buyer app ACKs
/on_confirm.
What is published: ONDC's retail contract spells out "Rules for order confirmation". /confirm and /on_confirm "should be idempotent", and the buyer app "will create the order id which will be unique across the network". On a valid /confirm, the seller app ACKs and "creates order (preferably with internal state to indicate order is being processed), but shouldn't forward to merchant". A validation failure is NACKed with 31002 and the buyer app cancels with reason code 999; after a retriable error (31001) or silence, the buyer app retries, with "a cap for no of attempts over specific duration", then cancels with 999. Once the buyer app ACKs /on_confirm, "SNP forwards order to merchant"; if the buyer app NACKs it (23002) or stays silent past the seller app's retries, the seller app cancels with 998, and meanwhile answers other calls with error 31003, "order processing in progress". Stale messages, older than one already processed for the same transaction_id and message_id, can be NACKed (20002 or 30022). ONDC's FAQ adds that when a cart spans several seller apps and one /confirm fails, "all associated orders in that transaction_id must be cancelled".
%% title: An order on both sides, or on neither stateDiagram-v2 [*] --> CONFIRM_SENT: buyer app creates order id, sends confirm CONFIRM_SENT --> HELD_AT_SELLER: seller ACKs, order held CONFIRM_SENT --> CANCELLED_999: NACK 31002 or no answer after retries HELD_AT_SELLER --> ACCEPTED: on_confirm ACKed, released to shop 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
- The buyer app owns the id; the seller app owns the state. One side names the order so retries collide; the other is the truth about fulfilment.
- Hold, then release. The ACK to
/confirmmeans "recorded", not "packing". Release to the shop only after the buyer app acknowledges/on_confirm. - Every failure has one terminal answer. No ACK → 999 by the buyer app; unacknowledged
/on_confirm→ 998 by the seller app. Both reach "cancelled" without talking: a saga whose compensations are written in the protocol. - An outbox per edge. The order row and its pending
/on_confirmcommit in one local transaction; a sender retries until ACKed. A crash between "saved" and "sent" can't lose a callback. - Terminal means frozen. ONDC's contract freezes an order at "Completed" or "Cancelled", except returns within the window, each a new fulfillment.
- Cancellation has a fallback. If the seller app doesn't answer
/cancelin time, the buyer app sends a "force" cancel; if that's ignored too, an IGM issue's resolution produces the/on_cancel. Fees follow terms published in/on_init.
# seller_app/confirm.py
"""
Handling /confirm at the seller app (illustrative). The order is created once per
network order id, held until the buyer app ACKs our /on_confirm, and released to the
shop only then. All outgoing callbacks go through an outbox in the same transaction.
"""
RETRY_WINDOW_S = 120 # our cap; ONDC leaves the cap to each participant
def on_confirm_request(db, msg) -> dict:
ctx, order = msg["context"], msg["message"]["order"]
key = (ctx["bap_id"], order["id"]) # network-unique order id
existing = db.orders.get(key)
if existing: # a retry: same order, same answer
return {"ack": "ACK"}
if db.seen_newer(ctx["transaction_id"], ctx["message_id"], ctx["timestamp"]):
return {"nack": "30022"} # stale request
problem = validate(db, order) # quote, stock, terms match on_init
if problem:
return {"nack": "31002", "message": problem} # buyer app cancels with 999
with db.tx():
db.orders.insert(key, order, state="HELD", txn=ctx["transaction_id"])
db.stock.reserve(order["items"], ref=key)
db.outbox.add(to=ctx["bap_uri"], action="on_confirm",
body=build_on_confirm(order, state="Accepted"), ref=key)
return {"ack": "ACK"}
def on_outbox_result(db, ref, acked: bool, age_s: float) -> None:
if acked:
db.orders.update(ref, state="Accepted")
release_to_merchant(ref) # only now does Rehana see it
elif age_s > RETRY_WINDOW_S:
with db.tx():
db.orders.update(ref, state="Cancelled", reason="998")
db.stock.release(ref=ref)
db.outbox.add(action="on_cancel", body=cancel_body(ref, "998"), ref=ref)Deep Dive 4: Logistics Is Just Another Participant (Week 5 Concepts: Saga Orchestration; Week 3: Dead Letters)#
"Rehana doesn't own a van. Her seller app buys delivery on the same network, from whichever logistics provider quotes best for Moradabad to Pune. So one retail order is really two linked transactions, and the seller app sits in the middle of both: seller to the buyer app, buyer to the logistics app. Every state the rider creates has to travel back through it."
This builds on Week 5, Day 3 · Saga Orchestration and Week 3, Day 4 · Dead Letters and Poison Pills.
The rider who didn't come
At 7:42, while quoting the Pune thali, Rehana's seller app got four logistics quotes and picked one at ₹68. At 8:30 that provider cancels: no rider free. The retail order is already confirmed and the buyer has paid ₹68 for delivery. What can the seller app do, and what does the buyer see? Two days later the courier can't find the Pune address and starts returning the parcel: which messages flow, between whom, and what happens to the retail order and the refund? And who pays if the replacement courier charges ₹90?
One strong answer
- Re-procure first: search for another provider and confirm with it; the buyer sees a later time in the next
/on_status. Only if nobody can take it does the seller app cancel and cascade a retail cancellation. - Return to origin (RTO): the provider sends
/on_cancelwith an RTO reason code; the seller app accepts a valid RTO and sends a cascaded retail/on_cancelwith a new RTO fulfillment; RTO runs to "RTO-Delivered" or "RTO-Disposed", and the refund follows the order's terms. - The ₹22: in our design, the seller app bears it. The retail contract says the order value "cannot increase through /on_update", so the buyer can't be charged more; and ONDC's FAQ, for the non-cascaded flow, says a seller app whose delivery estimate was lower than the provider's charge procures logistics and bears the cost.
What is published: ONDC's FAQ describes the "cascaded" flow: "once the retail buyer builds a cart (in /select call), the seller app starts searching for logistics service providers… Logistics buyer which is also the retail seller then goes ahead with one of the LSP and cascades back the delivery charges in /on_select of the retail contract." On retail /init it initialises the logistics order, "from then on, all subsequent calls are cascaded". If the provider delays pickup or cancels, the logistics buyer can cancel and cascade it back, or "search for another LSP"; in a non-cascaded flow the seller app bears any delivery cost above its estimate. The retail contract defines RTO: a logistics /on_cancel with an RTO reason code "will initiate RTO" and a cascaded retail /on_cancel with a new RTO fulfillment. ONDC's "Harmonizing statuses" note adds pickup and delivery confirmation codes (an AWB number or OTP). At network scale, ONDC reported 50,000+ delivery executives in 250+ cities, and a first-in, first-out model matching the first-ready order with the first-arriving rider that reached 25% of logistics orders and cut average pickup times from 15 minutes to under 6 (FY 2025–26).
%% title: One retail order, two linked transactions, one orchestrator in the middle
sequenceDiagram
autonumber
participant B as Buyer app
participant S as Seller app · orchestrator
participant L1 as Logistics app 1
participant L2 as Logistics app 2
B->>S: confirm O-77
S->>L1: logistics confirm, linked to O-77
S->>B: on_confirm Accepted
L1->>S: on_cancel · no rider
alt another provider available
S->>L2: logistics search, init, confirm
S->>B: on_status · new pickup time
L2->>S: on_status Order-picked-up
S->>B: on_status Order-picked-up
else nobody available
S->>B: on_cancel O-77 · refund per terms
end
Note over S,L2: RTO later? logistics on_cancel with RTO code · cascaded retail on_cancel with an RTO fulfillment- The seller app is the saga orchestrator. It holds both transactions and decides compensations: re-procure, cancel, or RTO. The buyer app never talks to the logistics app.
- Map states with a table. "Agent-assigned", "Order-picked-up", "Out-for-delivery", "Order-delivered" map to retail fulfilment states; a versioned table beats ad hoc translation.
- Forward every change, once. Each logistics
on_statusbecomes one retailon_statusthrough the outbox; the contract says batching "should not be done". - Dead-letter, don't loop. If the buyer app NACKs a cascaded status for bad schema, resending the same bytes won't help: park it, alert, fix, replay.
- Codes are the handover, and price is fixed at confirm. Pickup and delivery codes are evidence for disputes; any delivery-cost difference is between the seller app and the provider.
# seller_app/logistics_cascade.py
"""
Cascading logistics events into the retail order (illustrative). The seller app maps
each logistics state to the retail fulfillment state, re-procures on a pre-pickup
cancellation, and cascades RTO. Every outgoing callback goes through the outbox.
"""
STATE_MAP = {
"Agent-assigned": "Agent-assigned", "Order-picked-up": "Order-picked-up",
"Out-for-delivery": "Out-for-delivery", "Order-delivered": "Order-delivered",
}
def on_logistics_event(db, lo, event) -> None:
order = db.orders.by_logistics_order(lo.id)
if event.kind == "status":
retail_state = STATE_MAP.get(event.state)
if retail_state and db.fulfilment.advance(order.id, retail_state, at=event.ts):
db.outbox.add(action="on_status", body=status_body(order, retail_state), ref=order.id)
elif event.kind == "cancel" and event.is_rto:
with db.tx(): # a valid RTO must be accepted
db.orders.update(order.id, state="Cancelled", fulfilment="RTO-Initiated")
db.outbox.add(action="on_cancel", body=rto_body(order, event), ref=order.id)
elif event.kind == "cancel" and not db.fulfilment.picked_up(order.id):
alt = reprocure(order, exclude=lo.provider) # search, init, confirm with another LSP
if alt:
db.outbox.add(action="on_status", body=eta_body(order, alt.eta), ref=order.id)
else:
with db.tx():
db.orders.update(order.id, state="Cancelled", reason=event.reason)
db.outbox.add(action="on_cancel", body=cancel_body(order, event.reason), ref=order.id)Orders and logistics
Q1/3 The buyer app gets an ACK to /confirm but never receives /on_confirm. What does the seller app do with the order meanwhile?
Answer & explanation
Answer: B
ONDC's rule: create the order on ACK, but don't forward it to the merchant until /on_confirm is acknowledged.
Q2/3 A cart spans two seller apps; one /confirm fails. What does ONDC's FAQ say?
Answer & explanation
Answer: B
All-or-nothing across seller apps, by compensation.
Q3/3 The logistics provider cancels before pickup. What's the seller app's first move?
Answer & explanation
Answer: B
Re-procure if possible; cascade a cancellation only when no provider can take it.
Deep Dive 5: Settlement and Reconciliation Between Companies (Week 3 Concepts: Audit Logs; Week 5: Conflict Resolution; Week 2: Distributed Cron)#
"The buyer in Pune paid the payments app. Rehana is owed the thali's price minus two commissions, the payments app keeps its finder fee, and the courier is paid by the seller app in its own logistics transaction. Four parties, four bank accounts, and two companies' books that each think they know what's owed. If either side can move money on its own say-so, the other has to trust it. So neither does: a settlement agency moves money only when both sides' instructions match."
This builds on Week 3, Day 5 · Audit Log System, Week 5, Day 4 · Conflict Resolution and Week 2, Day 5 · Distributed Cron.
₹1,050, three days later
Take ONDC's own example. A retail order of ₹1,050 is collected by the buyer app. The buyer app's commission (its finder fee) is ₹50; the seller app's commission is ₹200. What does the buyer app's settlement instruction say, and what does the seller app's? When are they sent, and what does the settlement agency do with them? Then: the buyer app's instruction says ₹1,000 is owed to the seller app, but the seller app's says ₹1,010 because it forgot a ₹10 discount. What happens? And the buyer app sends nothing at all that day: does the seller app still get paid?
One strong answer
- Buyer app (collector): inter-participant ₹1,000 (owed to the seller app), collector ₹50 (its finder fee), self ₹50 (moved to its own operative account).
- Seller app (receiver): inter-participant ₹1,000, collector ₹50, self ₹200 (its commission), provider ₹800 (to Rehana's bank account).
- When: on the date the order's terms give (for example, delivery plus the settlement window), both send
/settlebefore the cycle's cut-off. - The agency matches them order by order, checks the collector's balance, instructs the banks, and answers both with
/on_settle: SETTLED or NOT_SETTLED. - ₹1,000 vs ₹1,010: NOT_SETTLED. The seller app sends
/reconwith its breakup; the buyer app answers/on_recon. Agreed orders settle next cycle; the rest become IGM issues (underpaid, overpaid, not paid). - Buyer app silent: under RSF 2.0 the seller app's claim is settled. ONDC's FAQ strongly suggests a daily
/settle, even an empty one, so money isn't debited on the seller app's word alone.
What is published: ONDC's Reconciliation and Settlement Framework (RSF 2.0, redesigned in 2024) names a collector (usually the buyer app in retail), a receiver (the seller app) and a settlement agency (ONDC's FAQ names NBBL, an NPCI subsidiary). Participants open an account for ONDC settlement and integrate six APIs: /settle, /on_settle, /report, /on_report, /recon and /on_recon. Both sides send /settle for orders due under the order's terms (settlement_basis, such as delivery, and settlement_window, such as P1D); the cycle runs daily. The NOCS procedural guidelines list the agency's checks (duplicates, participants, settlement accounts, "2-way reconciliation", balance), then a Multilateral Net Settlement Batch uploaded to RBI's RTGS platform at a cut-off, pair-wise payouts to sellers by NEFT, RTGS or IMPS, and /on_settle with a settlement reference per order. RSF 2.0 is "claim based rather than depending solely on Buyer Apps", unmatched orders are rejected, and the agency nets interbank movement while participants submit gross. ONDC's FAQ says retail usually settles after a terminal state, "for example return window plus 7 days", that the time allowed for an on_recon is typically "around 5-7 days", and that unresolved differences go to IGM; refunds after money has moved go through an IGM issue too.
%% title: Money moves only when both sides say the same thing
flowchart TD
C["Buyer app · collector<br/>/settle: inter-NP 1,000, self 50"]:::core
R["Seller app · receiver<br/>/settle: inter-NP 1,000, self 200, provider 800"]:::core
SA@{ icon: "sd:bank", label: "Settlement agency<br/>dedupe, validate, match, balance" }
M{"Both sides match<br/>for this order?"}
NET@{ icon: "sd:file", label: "Net batch to RTGS<br/>pair-wise payouts" }
NS["NOT_SETTLED<br/>recon and on_recon"]:::hot
IGM@{ icon: "sd:chat", label: "IGM issue<br/>underpaid, overpaid, not paid" }
SOR@{ icon: "sd:wallet", label: "Rehana's bank<br/>₹800" }
C ==> SA
R ==> SA
SA ==> M
M ==>|"yes"| NET ==> SOR
M -->|"no"| NS
NS -->|"agreed: next cycle"| SA
NS -.->|"still disputed"| IGM
class SA store
class NET store
class IGM service
class SOR external- Two ledgers, one matcher. Each participant keeps an append-only ledger of what each order owes. The agency never decides who's right; it moves money only when they agree.
- Derive amounts from the signed trail. Quote, finder fee, cancellation terms and every
/on_updateare signed messages both sides hold. Computed from the same trail, settlements agree by construction; mismatches mean bugs, missed updates or tax. - Settle after the terminal state. Waiting for "Completed" plus the return window makes most refunds a reduction, not a clawback.
- Daily is a cron with a deadline. One idempotent run per cycle before the cut-off: select orders due today, send
/settle, persist/on_settle, ask/reportif it doesn't arrive. - Reconcile three ways. Ledger against
/on_settle,/on_settlereferences against the bank statement, and own view against the counterparty's through/recon. - Gross in, net out. The agency nets every participant's gross orders into one batch, so a hundred participants don't need a hundred bilateral transfers.
# settlement_agency/match.py
"""
Matching collector and receiver instructions for one settlement cycle (illustrative).
Only orders where both sides' amounts agree (or where the collector stayed silent,
as RSF 2.0 provides) are settled, and only if the collector's account can fund them.
"""
from collections import defaultdict
def settle_cycle(claims, submissions, balances):
"""claims: receiver /settle lines; submissions: collector /settle lines; keyed by order id."""
by_order = {s.order_id: s for s in submissions}
silent = {c.collector for c in claims} - {s.collector for s in submissions}
matched, rejected = [], []
for c in claims:
s = by_order.get(c.order_id)
if s is None and c.collector in silent:
matched.append(c) # claim-based: collector sent nothing
elif s is None or (s.inter_np, s.collector_amt) != (c.inter_np, c.collector_amt):
rejected.append((c.order_id, "NOT_SETTLED")) # → /recon between the two NPs
else:
matched.append(c)
need = defaultdict(int)
for c in matched:
need[c.collector] += c.inter_np
funded = [c for c in matched if balances[c.collector] >= need[c.collector]]
short = [(c.order_id, "NOT_SETTLED") for c in matched if c not in funded]
return net_batch(funded), payouts(funded), rejected + short # → RTGS batch, NEFT/IMPS, /on_settleDeep Dive 6: Complaints and Health Across Company Lines (Week 10 Concepts: Observability, SLOs; Week 8: Event Ingestion)#
"When something goes wrong in a single company, the complaint goes to one support team that can see everything. Here the buyer complains to the app she used, which doesn't hold the order details the shop holds, can't see the rider, and has no authority over either. And when the network as a whole gets slower, nobody can see why, because nobody sees everything. Both problems need a protocol of their own."
This builds on Week 10, Day 2 · Observability, Week 10, Day 1 · SLIs, SLOs and SLAs and Week 8, Day 1 · Event Ingestion.
A dented diya in Kolkata
The Kolkata family's diyas arrive with one dented. They complain in their bank's app. Who must answer them, by when, and who actually decides whether they get a refund: the bank, Rehana's seller app, Rehana, or the courier? If the courier dented it, how does the complaint reach the courier? If the bank's app does nothing for three days, where can the family go? Separately: ONDC notices that confirmation rates on one seller app have dropped from 98% to 70% in an hour. How could it know that, without seeing anyone's orders?
One strong answer
- First response: the buyer app, within 48 hours, with a ticket number; resolution within 30 days, the limits ONDC cites from the Consumer Protection (E-commerce) Rules, 2020.
- Who decides: typically the seller or seller app; the buyer app coordinates. Over IGM, the bank's app raises an
issueto the seller app, which can cascade it to the logistics provider, whose grievance officer can propose a refund. - Escalation: after a missed 48 hours, a missed promise or 30 days, to ONDC's grievance portal (or the National Consumer Helpline), then online dispute resolution or a consumer court.
- Health without data: participants push masked protocol logs; ONDC computes per-participant metrics such as attempt-to-confirm rate and callback times, and flags the drop to that participant alone.
What is published: ONDC's complaints page says the buyer app "must provide you with this response within 48 hours", with a ticket number, and the complaint "has to be resolved within 30 days", both "under the E-commerce Rules 2020"; for product or delivery issues, typically, "the seller or Seller App will be responsible for resolving your complaint, while the Buyer App only facilitates". Unresolved complaints go to ONDC's grievance portal, then to "empanelled Online Dispute Resolution service providers" or consumer courts. IGM defines four APIs (issue, on_issue, issue_status, on_issue_status), issue types (issue, grievance, dispute), grievance officers (the interfacing participant's, the counterparty's and the cascaded counterparty's), and expected_response_time and expected_resolution_time fields; ONDC's FAQ adds that in a cascaded complaint "multiple eligible actors (like seller or lsp) can submit various resolution options". ONDC's September 2023 observability white paper has participants push logs "while masking personal identifiable information and business sensitive information", computes metrics like "attempt to confirm rate, fill rate, cancellation rate" and asynchronous response times, sends monthly scorecards, and promises that "at no point, a network participant would have access to sensitive insights pertaining to another network participant". Before going live, participants get their test logs approved, generated with ONDC's Pramaan tool or checked by its log validation utility.
%% title: A complaint crosses three companies, one append-only issue sequenceDiagram autonumber participant U as Kolkata buyer participant B as Bank's app · BAP participant S as Seller app participant L as Logistics app U->>B: one diya dented, photo B-->>U: ticket, resolve by Thursday B->>S: issue · item damaged, refs O-91 S-->>B: ACK S->>L: issue cascaded · damage in transit? L->>S: on_issue · resolution proposed, refund ₹120 S->>B: on_issue · refund ₹120 proposed B->>U: accept refund? U->>B: yes B->>S: issue · resolution accepted, close Note over B,S: refund flows through settlement · every action appended, signed, never edited
- One issue, append-only actions. Both sides hold copies; changes are appended actions with ids and references. Nobody edits history, so copies converge.
- Timers are data. Response and resolution deadlines are fields every participant's scheduler watches; a miss is an escalation event.
- Route by role. Each grievance officer talks only to its neighbours, as in the order flow.
- Observability is a protocol too. One masked event per protocol message (action, ids, timestamps, outcome) goes to a network collector, which computes the same metrics for everyone and shows each participant only its own.
- Measure the edges. Callback latency, NACKs by code, confirm success, cancellations by reason. In an async network a slow edge is invisible until orders silently die.
# network_obs/edge_metrics.py
"""
Per-participant edge metrics from masked protocol logs (illustrative). Each event has
the sender, receiver, action, transaction and message ids and timestamps; no buyer
data. Callback latency is measured from the request's ACK to its on_ callback.
"""
from statistics import quantiles
def edge_metrics(events, window):
req, lat, confirms, accepted = {}, [], 0, 0
for e in events.in_window(window):
if not e.action.startswith("on_"):
req[(e.transaction_id, e.message_id)] = e.ts # request seen
confirms += e.action == "confirm"
else:
t0 = req.pop((e.transaction_id, e.message_id), None)
if t0 is not None:
lat.append(e.ts - t0) # request → callback
accepted += e.action == "on_confirm" and e.order_state == "Accepted"
p95 = quantiles(lat, n=20)[-1] if len(lat) >= 20 else None
return {"confirm_success": accepted / confirms if confirms else None,
"callback_p95_s": p95,
"unanswered": len(req)} # requests with no callback yetMoney and trust
Q1/4 In ONDC's example, a ₹1,050 order has a ₹50 buyer-app fee and a ₹200 seller-app commission. What does the seller app's /settle send to the seller on record?
Answer & explanation
Answer: C
Inter-participant ₹1,000 from collector to receiver; of that, ₹200 is the seller app's and ₹800 goes to the provider.
Q2/4 The collector says ₹1,000 is owed; the receiver says ₹1,010. What happens?
Answer & explanation
Answer: C
The agency never decides who's right; it moves money only on a match.
Q3/4 A buyer app hasn't answered a complaint in 48 hours. Where can the buyer go?
Answer & explanation
Answer: B
48 hours to respond and 30 days to resolve, as ONDC's complaints page cites from the E-commerce Rules, 2020.
Q4/4 How can ONDC see that one seller app's confirmations fell to 70% without reading its orders?
Answer & explanation
Answer: C
Network observability computes edge metrics from masked events and shares each participant's own numbers only with it.
Phase 5: Scaling and Edge Cases (5 minutes)#
"Walk me through Rehana's Dhanteras from the network's side, then the cases that bite."
Rehana's Dhanteras#
- Morning · Full catalogue refreshes
- Buyer apps request catalogues through the gateway; Rehana's seller app answers each directly and then pushes changes every few minutes
- 7:41 pm · Four orders in four minutes
- Select, init and confirm go directly to her seller app; each delivery is bought inside the retail select
- 7:44 pm · One idol, two carts
- One select gets a quote, the other "not available"; the stock-out goes out in the next push
- 7:46 pm · A silent confirm
- The Pune app retries with the same order id; the order is created once and released after its on_confirm is acknowledged
- 8:10 pm · Pickup
- The rider reads the pickup code; statuses cascade from the logistics app through the seller app to each buyer app
- Days later · Settlement
- After the return window, both sides send /settle; the agency matches, nets and pays Rehana's bank; a dented diya is refunded through IGM
Edge Cases#
Edge case 1: A seller app goes down on festival night
Problem: A large seller app stops answering for 40 minutes at the peak.
Solution: Its sellers stay in cached catalogues (absence isn't deletion), but buyer apps trip a per-counterparty circuit breaker after repeated /select timeouts and stop offering its items until it answers. In-flight confirms end as 999 cancellations and refunds. No other seller app is affected.
Edge case 2: A participant's key is compromised
Problem: A buyer app's signing key leaks.
Solution: Publish a new key with a new unique_key_id and revoke the old one; receivers need short key-cache TTLs and refresh-on-failure, so messages signed with the old key are NACKed quickly.
Edge case 3: Clock skew
Problem: A seller app's clock drifts by a minute; its callbacks are rejected as stale.
Solution: NTP everywhere (ONDC's FAQ asks for timestamps within ±10 seconds) and a skew metric per counterparty that alerts before rejections start.
Edge case 4: The collector can't fund settlement
Problem: A buyer app's settlement account can't cover the day's matched orders.
Solution: The balance check fails and those orders are NOT_SETTLED until it's funded. ONDC's website lists a voluntary Payment Assurance Program, a refundable security deposit by buyer apps; as of October 2026 its page says it is "not live yet".
Scaling and edge cases
Q1/2 A seller app times out on every select for 10 minutes. What should buyer apps do?
Answer & explanation
Answer: B
The failure stays local to that seller app's transactions; the catalogue comes back the moment it does.
Q2/2 A buyer app's signing key leaks. What has to happen at receivers?
Answer & explanation
Answer: B
Revocation only works as fast as the slowest cache honours it.
Phase 6: Monitoring and Operations#
"Three views: a participant watching its own edges, a participant watching its money, and the network watching everyone's health without seeing anyone's data."
Monitoring Dashboard#
Edges
| Metric | Now |
|---|---|
| Requests received (searches, orders, ACKs of our callbacks) / ACKed | 240/s / 99.6% |
| NACKs by code (top: 30022 stale, 31002 validation) | 0.3% / 0.1% |
/on_select callback p95 (incl. logistics search) |
2.4 s |
/on_confirm callback p95 |
0.9 s |
| Outbox backlog / oldest unACKed callback | 312 / 41 s |
| Key-cache hit rate / registry lookups | 99.97% / 0.6/s |
Orders
- Confirms received: 4,210/h; released to shops: 4,152; cancelled 999 / 998: 31 / 9
- Logistics re-procured after provider cancel: 44; RTOs initiated today: 17
Catalogue
- Subscribed buyer apps: 23; incremental pushes pending ACK: 2 (one buyer app NACKing, schema)
SLOs for an Open Network#
SLO 1: Callbacks arrive
- Target: 99% of callbacks within their message's
ttl; p95/on_confirm< 2 s
SLO 2: Orders agree
- Target: zero orders released to a shop without an acknowledged
/on_confirm; zero duplicate orders per network order id; order state mismatches with counterparties (found by/statussampling) < 0.01%
SLO 3: Money matches
- Target: > 99.5% of due orders SETTLED in their first cycle; every NOT_SETTLED order in recon within 1 day; every
/on_settlereference matched to a bank statement line
SLO 4: Complaints are answered
- Target: 100% of issues acknowledged within 48 hours; 100% resolved or escalated within 30 days
Reading the dashboard
Q1/2 The outbox backlog climbs from 300 to 30,000 and the oldest unACKed callback is 9 minutes old, while requests in are normal. Where do you look?
Answer & explanation
Answer: B
In an async network, a stuck callback queue is a counterparty problem first. Isolate queues per destination.
Q2/2 Key-cache hit rate drops to 60% and registry lookups jump to 400 a second. Likeliest cause, and the risk?
Answer & explanation
Answer: B
Rate-limit-aware key caching is part of availability, not an optimisation.
Final Challenge: Dhanteras, 8:40 pm, a Seller App Goes Quiet#
The interviewer leans back. "One more."
"Festival night. At 8:40 pm a large grocery seller app (fictional, like everything in this scenario) deploys a change. It still ACKs every /confirm, but its /on_confirm sender has a bug: callbacks are written to its outbox and never sent. It also stops sending incremental catalogue updates. Buyers have paid. Tell me what happens at the buyer apps, at the seller app, to the shops, and to settlement next week, and what you'd change."
- The seller app serves ~3,000 shops (assumed); at the peak it receives ~60 confirms a minute (assumed)
- From 8:40 pm, it ACKs
/confirmbut sends no/on_confirm; incremental catalogue pushes stop - Buyer apps retry
/confirmwith the same order id and a capped retry window (assumed 2 minutes) - The bug is found at 9:25 pm and the sender restarts with ~2,700 callbacks in its outbox
What happens, and what do you change?
Decide what each buyer app does from 8:42, what the seller app's held orders become, what happens when the 2,700 callbacks are finally sent, what shops and buyers see, what settlement does next week, and what you'd add so this is caught at 8:45, not 9:25.
One strong answer
- Buyer apps from 8:42: no
/on_confirmwithin the retry window, so each order is cancelled with 999 and refunded. Repeated failures trip each buyer app's breaker for that seller app, so they stop taking new orders for it. - Held orders: never released to shops, because release waits for an ACK of
/on_confirm. That rule just saved ~2,700 phantom orders. - At 9:25: replaying 2,700 stale
/on_confirms gets NACKs and then 998 cancellations. Better: check each order's age against the buyer apps' retry windows and cancel anything past it instead of sending it. - Catalogue: caches are 45 minutes stale, so more selects fail; when pushes resume, send all changes since 8:40.
- Settlement: cancelled orders never reach "Completed", so nobody sends
/settlefor them; nothing to reconcile if both sides recorded the same codes. - Catch it at 8:45: alert on "confirms ACKed without an
/on_confirmsent" and on outbox age per destination; let network observability flag the falling attempt-to-confirm rate within minutes; and canary deploys that watch both numbers.
Final challenge · Quest check
Q1/3 The seller app ACKs /confirm but never sends /on_confirm. Why don't shops pack orders the buyer apps have cancelled?
Answer & explanation
Answer: B
Hold, then release: the rule that makes a silent failure a refund, not a phantom delivery.
Q2/3 2,700 delayed /on_confirm callbacks are sent at 9:25. What should have happened instead?
Answer & explanation
Answer: B
A replay after a long outage should be reconciled against what the other side has certainly done by now.
Q3/3 What alert would have caught this in minutes?
Answer & explanation
Answer: B
Watch the edges: the system looked healthy from inside because every request was ACKed.
Interview Conclusion#
"Why not just have ONDC run a central order database that everyone writes to? It'd make consistency easy."
"It'd make consistency easy and the network pointless. A central order store makes the operator a platform: every participant depends on its availability, its schema, its speed and its policies, and it sees every order. The point of an open network is that a payments app and a seller platform can transact without a third party in the middle of each order. So consistency has to come from the protocol: one side names the order, both sides make every message idempotent, failures have deterministic outcomes both sides apply on their own, and money moves only when both sides' books match at a neutral agency. That's harder to build, but it's what lets anyone join."
"The single most important decision?"
"Holding the order at the seller app until the buyer app acknowledges the confirmation. It's one rule, and it means that whatever fails between two companies, the shop never fulfils an order the buyer's app doesn't have, and the buyer is never charged for one the shop doesn't."
Summary: Concepts Applied from 10-Week Course#
- Week 2: Failure-first design
- Timeout hell — partial search results until the message's ttl; capped confirm retries
- Idempotency — network-unique order ids; idempotent request and callback pairs; settlement ids
- Webhook delivery — every callback is a signed, retried, deduplicated webhook between companies
- Distributed cron — the daily settlement job before the cut-off
- Week 3: Messaging and async processing
- Transactional outbox — order rows and their callbacks committed together
- Dead letters — cascaded messages that keep failing are parked, fixed and replayed
- Audit logs — append-only ledgers and issue actions; signed messages as evidence
- Week 4: Caching
- Cache invalidation — catalogue caches refreshed by timestamped full and incremental updates; stock as a hint
- Week 5: Consistency and coordination
- Sagas — confirm and its compensations (999, 998) across two companies; logistics re-procurement and RTO orchestrated by the seller app
- Conflict resolution — two-sided settlement matching, then recon, then IGM
- Week 7: Search
- Indexing pipeline — buyer apps build their own index from catalogue refreshes
- Week 8: Analytics pipelines
- Event ingestion — masked protocol logs into network observability
- Week 9: Security
- Registry, Ed25519 signatures over BLAKE-512 digests, key rotation and revocation
- Week 10: Production readiness
- Edge metrics, participant scorecards, SLOs for callbacks, orders, money and complaints
Why ONDC Matters#
Consistency without a shared database
- Most systems get consistency from a database. An open network gets it from rules every participant follows on its own: who names the order, what a retry means, what silence means, when an order is released, when money moves. The protocol is the transaction manager
Trust is engineered, not assumed
- Strangers transact because every message is signed against a public registry, every rupee moves only when two ledgers match at a neutral agency, and every complaint has a deadline and an escalation path. Each of those is a system someone had to design
The load is in the broadcasts
- ONDC's order rate is modest (218 million transactions in FY 2025–26, about 7 a second on average, across retail, mobility, transit and finance). The expensive parts are discovery, which is a broadcast, and the cost of every participant re-implementing the same protocol correctly
Openness has a price, and a payoff
- An open network has more failure modes than a platform: more companies, more edges, more versions of the spec in production. In return, a brass seller in Moradabad can be found from a payments app, a bank and a grocery app at once, and a metro operator can sell tickets through dozens of apps with one integration
"Name the order on one side, keep it on both, release it only when both have heard, and move money only when both books agree."
Self-Assessment Checklist#
After studying this case study, you should be able to:
Protocol and trust:
- Explain the asynchronous request, ACK and
on_callback pattern, and why the synchronous reply is only an acknowledgement - Describe how a participant joins a registry, and how requests are signed (BLAKE-512 digest, Ed25519) and verified with cached keys
- Plan key rotation and revocation across hundreds of independent receivers
Discovery and orders:
- Explain why catalogue caching with full and incremental refresh beats live broadcast search, and how to order refreshes by timestamp
- Walk through confirm and on_confirm failures and the deterministic outcomes (999, 998) that keep two companies' orders consistent
- Orchestrate a cascaded logistics transaction, including re-procurement and RTO
Money and operations:
- Build collector and receiver settlement instructions for an order and explain two-sided matching, recon and escalation
- Route a complaint across buyer app, seller app and logistics provider with deadlines
- Define edge metrics and SLOs for an asynchronous multi-party network without sharing participants' data
Sources#
ONDC (ondc.org and ONDC publications):
- Read
ONDC — Our journey (first order on 29 April 2022 in Bengaluru; Section 8 company in December 2021; 1M+ transactions May 2023, 10M+ August 2023, 100M+ August 2024, 250M+ June 2025, 500M on 25 July 2026; 50+ logistics partners incl. 30+ small fleet operators, June 2026), accessed October 2026: https://ondc.org/pages/about-us.html
- Paper
ONDC — Building Bharat's Digital Commerce Infrastructure: FY 2025–26 in Focus (218 million transactions in FY 2025–26; 50,000+ delivery executives across 250+ cities; small fleet operators 35% of fulfilled orders; FIFO model 25% of logistics orders, pickup times from 15 to under 6 minutes; 300,000 metro and bus tickets a day), 2026: https://ondc.org/public/assets/newsletters/ondc-fy25-26-in-focus.pdf
- Read
ONDC — Complaints (buyer app to respond within 48 hours and resolve within 30 days under the E-commerce Rules 2020; escalation to ONDC's grievance portal; ODR), accessed October 2026: https://ondc.org/pages/complaint.html
- Read
ONDC — Gateway (gateways multicast search requests to seller applications), accessed October 2026: https://ondc.org/pages/gateway.html
- Book
ONDC — Payment Assurance Program ("not live yet"; refundable security deposit by buyer participants), accessed October 2026: https://ondc.org/pages/payment-assurance-program.html
- Paper
ONDC — White Paper: Strengthening the Health and Growth of India's E-commerce in an open network (network observability; masked logs; metrics; scorecards; data governance), September 2023: https://ondc-static-website-media.s3.ap-south-1.amazonaws.com/ondc-website-media/downloads/governance-and-policies/ONDC%20-%20White%20Paper%20-%20Strengthening%20the%20Health%20and%20Growth%20of%20India%E2%80%99s%20E-commerce%20in%20an%20open%20network%20-%20Sept%202023.pdf
- Paper
ONDC — RSF 2.0 (Reconciliation & Settlement Framework) explainer and BRD (roles; claim-based settlement; gross submissions, net movement; edge cases), April–August 2024: https://ondc-static-website-media.s3.ap-south-1.amazonaws.com/res/daea2fs3n/image/upload/ondc-website/files/esf_2_0_explainer_and_brd_02_08_24.pdf
- Paper
ONDC and NBBL — NOCS Procedural Guidelines, version 2.0 draft, 13 November 2024 (settlement steps; validations; Multilateral Net Settlement Batch on RBI RTGS; Process 1 and 2 payouts; /on_settle references): https://ondc-static-website-media.s3.ap-south-1.amazonaws.com/ondc-website-media/downloads/resource/nocs_procedural_guidelines.pdf
- Read
ONDC — Reconciliation & Settlement Framework page (archived 9 June 2026): https://web.archive.org/web/20260609102854/https://resources.ondc.org/reconsiliationandsettlementframework
ONDC protocol and developer documentation (github.com/ONDC-Official):
- Docs
developer-docs — Signing and verification (every request/callback pair a "contract"; BLAKE-512 digest; Ed25519; keyId format; X-Gateway-Authorization; NACK when no valid key): https://github.com/ONDC-Official/developer-docs/blob/main/registry/signing-verification.md
- Docs
developer-docs — Onboarding of Participants (FQDN, SSL and OCSP; Ed25519 and X25519 key pairs; site verification; /on_subscribe challenge; /lookup; recommended registry rate limits): https://github.com/ONDC-Official/developer-docs/blob/main/registry/Onboarding%20of%20Participants.md
- Docs
developer-docs — Auth header signing and verification (hash the raw payload; HTTP 401 on failure): https://github.com/ONDC-Official/developer-docs/blob/main/protocol-network-extension/Auth%20Header%20-%20signing%20and%20verification.md
- Docs
ONDC — Retail API contract 1.2.0 (full and incremental catalogue refresh; catalogue caching; finder fee in search; rules for order confirmation, error codes 31001, 31002, 31003, 23001, 23002, 20002, 30022 and reason codes 998, 999; terminal states; force cancellation; RTO), linked from: https://github.com/ONDC-Official/developer-docs/blob/main/protocol-network-extension/api-contracts/retail/All%20RET%20Domains.md
- Docs
ONDC-FAQs — Technical architecture, registry, logistics, RSF, IGM, getting started and network observability FAQs (gateway multicasts and doesn't return responses; only discovery via gateway; collected_by; transaction_id and message_id; ttl; ±10 s timestamps; cascaded logistics; RSF APIs, example amounts, daily cycle, "return window plus 7 days", on_recon TTL around 5–7 days; multiple resolution options in a cascaded complaint; both NPs push observability logs; Pramaan and log validation): https://github.com/ONDC-Official/ONDC-FAQs
- Book
ONDC-Protocol-Specs — Payment and Settlement Protocol v0.6 draft (collection by buyer app, seller app or logistics agent for COD; withholding amount) and Harmonizing statuses in cascaded retail & logistics transactions (pickup and delivery confirmation codes), 2022: https://github.com/ONDC-Official/ONDC-Protocol-Specs
- Docs
ONDC-NTS-Specifications — IGM release (issue, on_issue, issue_status, on_issue_status): https://github.com/ONDC-Official/ONDC-NTS-Specifications
- Read
protocol-network-extension — IGM enumerations (issue types, categories, GRO types, statuses): https://github.com/ONDC-Official/protocol-network-extension/tree/main/enums/igm
Beckn protocol:
- Docs
Beckn Protocol — BECKN-RFC-003, Beckn Protocol Communication (server-to-server, asynchronous; ACK then on_ callback; gateway multicast; multiple on_status), updated 2 July 2024: https://github.com/beckn/protocol-specifications/blob/master/docs/BECKN-003-Beckn-Protocol-Communication-Draft-01.md
Government of India:
- Read
PIB — ONDC Enables Fair, Transparent and Inclusive E-Commerce… (1.16 lakh+ retail sellers live from 630+ cities and towns as of 9 December 2025; seller apps' full catalogues discoverable to all buyer apps; buyer apps disclose ranking parameters), Lok Sabha written reply, 16 December 2025: https://www.pib.gov.in/PressReleasePage.aspx?PRID=2204664
- Paper
PIB — Digital Infrastructure in India factsheet (154.4 million+ cumulative orders and 15.4 million+ in December 2024; ~4.9 lakh a day; 7.64 lakh+ sellers and service providers; 616+ cities), February 2025: https://static.pib.gov.in/WriteReadData/specificdocs/documents/2025/feb/doc202521494701.pdf
Reporting:
- Read
Business Today — ONDC clocks 1.2 million transactions during Diwali week (nearly 1.2 million, 6–13 November 2023; 600+ cities), 17 November 2023: https://www.businesstoday.in/entrepreneurship/start-up/story/ondc-clocks-12-million-transactions-during-diwali-week-406132-2023-11-17
- Read
Inc42 — Riding On Festive Season, ONDC Logs 14 Mn Transactions In October (5.5 million mobility, 8.4 million non-mobility, October 2024; Inc42 cites a Moneycontrol report quoting an unnamed industry source), 5 November 2024: https://inc42.com/buzz/riding-on-festive-season-ondc-logs-14-mn-transactions-in-october
- Read
Bloomberg via Al Jazeera — In India, a new platform to curb the powers of Amazon, Flipkart (Nilekani, "hired as an adviser to ONDC last summer", wants to do for e-commerce what UPI did for payments), 28 April 2022: https://www.aljazeera.com/economy/2022/4/28/this-indian-billionaire-is-working-to-curb-the-powers-of-amazon
Design note: the services inside each participant, the code, thresholds, retry windows, dashboards and every number labelled as an assumption or estimate are an illustrative design, not any participant's or ONDC's internal systems. Seller apps per city, searches a second, catalogue sizes, messages per order, the festival peak and the final challenge's figures are assumptions. Rehana Ansari, Ansari Brass Works and her orders are fictional, and the buyer apps in her story are described, not real. ONDC's figures come from publications between 2022 and 2026; ONDC's transaction counts include mobility, transit tickets and financial services as well as retail. Consumer-protection and settlement rules are summarised for engineering context and should be checked against current ONDC policy and law.
Further Reading#
Related systems to study:
- Read
UPI (Bonus Problem 1 in this series): the open payments network ONDC is often compared to; one atomic transfer through a central switch, where ONDC has no switch in the order path
- Read
Razorpay (Bonus Problem 18): payments, settlement and reconciliation from a gateway's side
- Read
Blinkit (Bonus Problem 12) and Swiggy (Bonus Problem 17): one platform owning catalogue, orders and dispatch, the opposite of an unbundled network
- Read
Flipkart Big Billion Days (Bonus Problem 27): a single marketplace's festival sale, for contrast with a festival on an open network
Foundational concepts (this course):
- Read
Week 2, Day 1 — Timeout Hell
- Read
Week 2, Day 2 — Idempotency in Practice
- Read
Week 2, Day 4 — Webhook Delivery
- Read
Week 2, Day 5 — Distributed Cron
- Read
Week 3, Day 2 — Transactional Outbox
- Read
Week 3, Day 4 — Dead Letters and Poison Pills
- Read
Week 3, Day 5 — Audit Log System
- Read
Week 4, Day 2 — Cache Invalidation
- Read
Week 5, Day 2 — Saga Pattern
- Read
Week 5, Day 3 — Saga Orchestration
- Read
Week 5, Day 4 — Conflict Resolution
- Read
Week 7, Day 2 — Indexing Pipeline
- Read
Week 8, Day 1 — Event Ingestion
- Read
Week 9, Day 5 — Security Architecture
- Read
Week 10, Day 1 — SLIs, SLOs and SLAs
- Read
Week 10, Day 2 — Observability
"Name the order on one side, keep it on both, release it only when both have heard, and move money only when both books agree."
this lesson in shorts · 4
play them all →One idea from this lesson in about a minute each, made for your phone.
- 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.



