Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Every money operation in your system must be idempotent. If the client retries due to a network error, you must NOT create a second charge. The standard pattern is the idempotency-key header (Stripe-popularized) — a UUID that the client generates per-intent and the server stores keyed to. Forgetting this turns a 5xx network glitch into a double-charged customer.
Idempotency primitive.
Use these three in order. Each builds on the one before.
In one paragraph, explain idempotency and why money operations need it.
Walk me through the idempotency-key flow — client generates, server checks, cache stores.
Given a payment service receiving 10k req/sec where idempotency keys live in Redis, design the strategy for handling cache outages without dropping idempotency guarantees.
// Standard pattern: client sends an idempotency-key header per intent
// Server checks if this key has been seen; if so, return cached response.
import { Request, Response } from "express";
const KEY_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
interface IdempotencyEntry {
status: number;
body: object;
createdAt: number;
}
const cache = new Map<string, IdempotencyEntry>(); // → Redis in prod
async function chargeWithIdempotency(req: Request, res: Response) {
const key = req.header("Idempotency-Key");
if (!key) return res.status(400).json({ error: "Idempotency-Key required" });
// Check cache
const cached = cache.get(key);
if (cached && Date.now() - cached.createdAt < KEY_TTL_MS) {
return res.status(cached.status).json(cached.body);
}
// Reserve the slot (mark as "in progress" to prevent concurrent dupes)
cache.set(key, { status: 202, body: { status: "pending" }, createdAt: Date.now() });
try {
// The actual money operation
const charge = await stripe.paymentIntents.create({
amount: req.body.amount,
currency: req.body.currency,
payment_method: req.body.payment_method,
}, { idempotencyKey: key }); // Pass it down to Stripe too
cache.set(key, { status: 200, body: charge, createdAt: Date.now() });
return res.status(200).json(charge);
} catch (err) {
// Don't cache 5xx errors — let the client retry safely
cache.delete(key);
throw err;
}
}
// Key rules:
// - Client generates key (UUID v4 or similar); never server-generated
// - Key valid for at least 24 hours
// - Same key + same request body → same response
// - Same key + DIFFERENT request body → 409 Conflict
// - In Redis or DB, NOT in-memory (survives restarts)