01 / Background
ONDC — the Open Network for Digital Commerce — is India's government-backed open protocol for digital commerce. The Beckn Protocol underneath it defines how Buyer Apps (BAPs) and Provider Apps (BPPs) communicate to execute transactions — including financial products like personal loans.
I joined the fintech company as the sole developer when ONDC had barely launched its financial services vertical. There were no tutorials, no SDKs, no Stack Overflow answers. Everything had to be reverse-engineered from the raw Beckn Protocol specification PDF and live network payload logs.
My task: build a complete BAP/BPP connector that could onboard credit providers and handle the full personal loan lifecycle — application, underwriting, approval, and disbursal — on the ONDC network.
02 / How ONDC Loan Flow Works
A personal loan on ONDC flows through a series of state transitions, each triggered by a webhook from the Beckn Gateway. The sequence looks roughly like this:
Each of these webhook calls arrives at your Node.js server from the ONDC Gateway. The problem is that the gateway does not guarantee exactly-once delivery. Under load — or during network hiccups — the same webhook fires multiple times within milliseconds.
03 / The Race Condition
Consider the /on_confirm webhook — the one that tells your BPP that the loan has been sanctioned and disbursal should begin.
Now imagine two identical /on_confirm payloads arrive at your server 40 milliseconds apart. Both carry the same transaction ID, same loan amount, same borrower. Your Node.js server receives both, spins up two async handlers simultaneously, and both race toward your PostgreSQL database to write the disbursal record.
What happens next depends entirely on whether you built the right defences. Without them — both handlers write successfully. The loan disburses twice. The borrower gets double the money. The lender loses it.
This is not a hypothetical. During load testing with the ONDC preprod environment, I saw the same webhook arrive 3 times in under 100ms. In a financial system, this is catastrophic.
04 / The Two-Layer Defence
I solved this with two independent layers of protection. The key insight is that no single layer is sufficient — you need defence in depth because different failure modes require different solutions.
Layer 1 — Idempotency at the API Gateway (Redis)
Every incoming webhook payload is hashed into a unique idempotency key using the transaction ID, the Beckn action type, and a hash of the payload body. Before the handler runs any business logic, it checks Redis for this key.
When the duplicate arrives 40ms after the first, Redis already has the idempotency key. The middleware returns the cached successful response in under 1ms — without touching PostgreSQL, without running business logic, without any side effects. From the gateway's perspective, both webhooks succeeded. Only one actually processed.
The Redis key has a TTL of 24 hours — long enough to catch any delayed retries, short enough that the key store doesn't grow indefinitely.
Layer 2 — Database Row Locks (PostgreSQL)
Redis is fast but it is not a transaction system. Under extreme load, two handlers could theoretically both pass the Redis check before either has written the key. This is the TOCTOU problem — Time Of Check, Time Of Use.
For any operation that mutates financial state — writing a disbursal, updating a loan status, recording a transaction — I wrap the entire operation in a PostgreSQL transaction with a row-level lock.
The SELECT ... FOR UPDATE acquires a row-level lock. If a second thread reaches this line while the first still holds the lock, it blocks — it cannot read the row, cannot write to it, cannot proceed. When the first thread commits and releases the lock, the second thread reads the now-updated status (disbursed) and exits cleanly without writing a duplicate record.
Even if Redis completely failed, this layer would catch the duplicate. Even if both threads passed Redis, this layer would catch the duplicate. Two independent mechanisms, both protecting the same critical section.
05 / Why Both Layers Are Necessary
A natural question: if PostgreSQL row locks are so reliable, why bother with Redis at all?
Because database locks are expensive under load. Every SELECT ... FOR UPDATEholds a connection from the pool. If 50 duplicate webhooks arrive simultaneously, 49 of them block on the database lock — holding 49 connections, adding latency, degrading throughput for every other query running simultaneously.
Redis eliminates 99% of duplicates before they reach the database — in under 1ms, with no connection pool pressure. PostgreSQL handles the remaining 1% edge case where Redis was somehow bypassed. Both layers together give you speed and correctness.
06 / What Building on ONDC Taught Me
ONDC's financial services vertical had almost no developer documentation when I built this. The Beckn Protocol spec is dense and abstract. Real payload structures only became clear by intercepting live traffic from the preprod gateway and logging everything.
Three things I learned that apply beyond ONDC:
Read the protocol spec, not the tutorial
Tutorials are interpretations that can be wrong or outdated. The raw protocol specification is the ground truth. It is harder to read but it never lies.
Design for failure at every network boundary
Any external system — gateway, payment provider, RTA — can and will send you the same event multiple times. Idempotency is not optional in financial systems. It is the default.
Two independent defences beat one perfect defence
Redis could fail. PostgreSQL locks can be misconfigured. A single layer of protection has a single point of failure. Two layers, each solving the same problem differently, give you real safety.
07 / Current Status
The personal loan connector is live on the ONDC network. Purchase Finance and Working Capital connectors are built on the same idempotency infrastructure and going live next.
The same two-layer pattern — Redis idempotency key + PostgreSQL row lock — is now the standard for every state-mutating operation across the entire fintech platform. What started as a solution to one race condition became the architectural pattern for the whole system.
Next Article
RTA Mailback Pipeline →