A tiny demo showing why retries cause double charges and how idempotency + a state machine prevents them, using a simple Node/Express backend and a lightweight HTML/Tailwind frontend.
- 🎥 Video walkthrough: 🔧 Add YouTube link here
- 📺 DuaLearn channel: 🔧 Add channel link here
- Follow along for more practical, production-grade patterns.
- Problem (Unsafe): Under server hiccups (503 +
Retry-After), clients retry — and if your API isn’t idempotent, each retry executes a fresh debit → double spend. - Solution (Safe): The client sends an Idempotency-Key per user intent. The server creates one
PENDINGtransfer, returns 503 while it’s in progress, then finalizes once on the last attempt → single debit. Further retries reuse the same result.
- Real users (and SDKs) do retry on lag/timeouts or 5xx.
- Without idempotency, retries create duplicate charges, angry users, and reconciliation pain.
- With idempotency + a tiny state machine (
PENDING → COMPLETED) you get correctness under retries, crashes, and parallel clicks.
- Both flows simulate hiccups with three
503 Service Unavailable+Retry-After: 1, then success on the 4th attempt. POST /unsafe-pay: debits every attempt → multiple transfers, balance drops multiple times.POST /safe-pay: first attempt reserves once (PENDING), retries reuse the same transferId, final attempt flips toCOMPLETED→ balance drops once.GET /balance: returns all users and their balances for the UI table.
GET /balance→[{ email, name?, balance }]POST /unsafe-payBody:{ senderEmail, recipientEmail, amount }Returns: 503 (x3) then201 { status:"COMPLETED", transferId, newBalance }POST /safe-payBody:{ senderEmail, recipientEmail, amount, idempotencyKey }Returns: 503 (x3) with{ status:"processing", idempotencyKey, transferId }, then201 { status:"COMPLETED", transferId, newBalance }Errors:409if same key with different payload;404/409for not found/insufficient funds
- Balances table: live balances for all users (auto-refreshes during runs)
- Selectors: choose Sender & Recipient from dropdowns, set amount
- Unsafe run: watch balance drop every retry (double spend)
- Safe run: one
transferIdgoes Pending → Completed, balance drops once - Parallel Safe (5): 5 concurrent requests with the same key converge to one result
- Clone & install
git clone https://github.com/pavankpdev/idempotency-implementation.git
cd idempotency-implementation
pnpm install- Run the backend
pnpm dev- Open the frontend
- Visit:
http://localhost:3001(🔧 adjust if you use another port)
- Try the flows
- Click Run Unsafe Demo → observe multiple debits
- Click Run Safe Demo → observe one debit (same Idempotency-Key)
- Try Parallel Safe (5) → all converge to a single transfer
- Backend: Node/Express + simple JSON storage (e.g., lowdb)
- Hiccup simulation: three 503s +
Retry-After: 1, then success - Safe path uses a per-key lock and idempotent finalize step to prevent races
