Building Reliable Payment APIs
Sujal Vijayvargiya
SDE-1
Idempotency, state machines, and reconciliation — the three pillars of payment API reliability I've learned building high-scale transaction flows at Juspay.
Idempotency Is Non-Negotiable
Networks fail. Clients retry. In payment systems, processing the same transaction twice is a catastrophic failure. Every mutation endpoint that touches financial state must require an idempotency key and store a deduplicated result before executing downstream side effects.
Use atomic database upserts or Redis SET NX locks — not application-level checks — to guarantee safety under concurrent retries.
// Every payment mutation requires an idempotency key
async function processPayment(key: string, tx: Transaction): Promise<Receipt> {
const existing = await db.idempotencyStore.findOne(key);
if (existing) return existing.result; // Deduplicated
const lock = await redis.setNX(`lock:${key}`, 1, 30_000);
if (!lock) throw new ConcurrentRequestError();
try {
const receipt = await executePayment(tx);
await db.idempotencyStore.upsert(key, receipt);
return receipt;
} finally {
await redis.del(`lock:${key}`);
}
}State Machines Over Boolean Flags
Payment transactions have complex lifecycles: pending, authorized, captured, refunded, failed. Representing this with boolean flags (isAuthorized, isCaptured, isRefunded) creates exponentially many invalid combinations.
Explicit state machines with transition guards eliminate impossible states entirely. When I redesigned Juspay's pre-auth platform with formal state transitions, a whole class of race condition bugs simply ceased to exist.
Build Reconciliation From Day One
External payment gateways are eventual systems. Your internal state and the gateway's state will diverge — guaranteed. Build reconciliation jobs from the start, not as an afterthought. For our mandate migration work, reconciliation wasn't a monitoring concern — it was the core product guarantee.
— END OF TRANSMISSION —
RETURN TO BLOG