Picture this classic e-commerce nightmare: A customer is riding the subway with a spotty 4G connection. They tap "Pay 2,500 TL". The request reaches your backend and charges their card, but the response drops before reaching their phone. Frustrated, they tap the button two more times—or their HTTP client automatically retries.
Ten seconds later, they receive three SMS notifications from their bank: their credit card was charged three times for the exact same order.
1. Why "Just Disable the Button in React" Fails in Production
Junior developers often respond to this scenario by saying: "Easy! Just set disabled={isLoading} on the submit button in the frontend."
While disabling the button is good UX, it never guarantees backend safety:
- Network-Level Retries: Mobile apps, API gateways, and webhook senders automatically retry requests when a TCP connection times out.
- Multi-Tab / Multi-Device Requests: A user can open checkout in two tabs or bypass frontend UI constraints altogether.
- The Two Generals' Problem: When a network connection drops mid-flight, the client has no way of knowing whether the server failed before charging the card or after charging the card.
To solve this safely, the payment operation must be Idempotent.
2. What is Idempotency (and an Idempotency Key)?
In mathematics and computer science, an operation is idempotent if performing it once produces the exact same system state as performing it ten times.
GET /orders/123andDELETE /orders/123are naturally idempotent.POST /payments/chargeis NOT naturally idempotent—every call creates a new charge.
To make POST /payments/charge idempotent, the client generates a unique Idempotency Key (typically a UUIDv4 tied to that specific checkout intent) and sends it in the HTTP header:
POST /api/v1/payments/charge HTTP/1.1
Host: api.czay.dev
Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
Content-Type: application/json
{
"orderId": "ord_8841",
"amount": 250000,
"currency": "TRY"
}3. Step-by-Step Backend Implementation (Redis + Database)
When your backend receives a request with an Idempotency-Key, it executes a strict atomic state machine:
export async function processPayment(req, res) {
const key = req.headers['idempotency-key'];
if (!key) return res.status(400).json({ error: 'Missing Idempotency-Key header' });
const redisKey = `idempotency:${req.user.id}:${key}`;
// 1. Atomic Lock (SET if Not Exists with 24h TTL)
const acquired = await redis.set(redisKey, JSON.stringify({ status: 'IN_PROGRESS' }), 'EX', 86400, 'NX');
if (!acquired) {
const existing = JSON.parse(await redis.get(redisKey));
// If the first request is still talking to the bank right now:
if (existing.status === 'IN_PROGRESS') {
return res.status(409).json({ error: 'Payment is currently being processed. Please wait.' });
}
// If the first request already succeeded, return the cached receipt without charging again!
return res.status(existing.statusCode).json(existing.body);
}
try {
// 2. Execute the actual payment gateway call (iyzico / PayTR / Stripe)
const paymentResult = await paymentGateway.charge(req.body);
// 3. Cache the final response under the Idempotency Key
const responsePayload = { status: 'COMPLETED', statusCode: 200, body: paymentResult };
await redis.set(redisKey, JSON.stringify(responsePayload), 'EX', 86400);
return res.status(200).json(paymentResult);
} catch (err) {
// If a transient network error occurred before reaching the bank, release the lock so retry is allowed
await redis.del(redisKey);
throw err;
}
}Info: Always Back Redis with a Database Unique Constraint
In addition to the fast Redis lock (SET NX), always add a UNIQUE index on idempotency_key (or order_id for completed charges) in your PostgreSQL payments table as your ultimate ACID safety net.
Interactive Roadmap & Tool
System Design: Scaling to a Million Users
Adding servers isn't enough: a stage-by-stage System Design map of why systems fall over on a big day and how load is cut before it reaches the database.
SQL or NoSQL?
Answer six questions and I'll tell you which kind of database fits your project, and why.
Read next
- CZENWhat is Dokku? Slashing a $200 Cloud Bill to a $5 VPS with Open-Source PaaS Architecture
What is Dokku? Slashing a $200 Cloud Bill to a $5 VPS with Open-Source PaaS Architecture
October 25, 2026 · 5 min read - CZENCutting Through the Noise: Which Backend Language and Framework Should You Choose in 2026?
Cutting Through the Noise: Which Backend Language and Framework Should You Choose in 2026?
October 25, 2026 · 4 min read