Introduction
When clients retry due to network failures, the same order gets created twice. Use Idempotency Keys to ensure the same request always returns the same response and side effects execute only once. Generate designs with Claude Code.
CLAUDE.md Idempotency Rules
## Idempotency Design Rules
### Idempotency Key
- Require Idempotency-Key header for POST requests (financial, orders)
- Key expiry: 24 hours
- Cache responses and return the same response
- Return 409 for in-flight requests (prevent duplicate execution)
### Idempotent Operation Design
- DB: INSERT ... ON CONFLICT DO NOTHING
- External API: derive Idempotency Key from your own key
- Message queue: use fixed jobId to prevent duplicate queuing
### Retry-Safe Operations
- GET/PUT/DELETE: inherently idempotent
- POST: make idempotent with Idempotency Key
- External billing (Stripe): always use external Idempotency Key
Generated Idempotency Middleware
// src/middleware/idempotency.ts
export function idempotencyMiddleware(options: { required?: boolean } = {}) {
return async (req: Request, res: Response, next: NextFunction) => {
if (['GET', 'HEAD', 'OPTIONS', 'DELETE', 'PUT'].includes(req.method)) return next();
const idempotencyKey = req.headers['idempotency-key'] as string | undefined;
if (!idempotencyKey) {
if (options.required) return res.status(400).json({ error: 'Idempotency-Key header is required' });
return next();
}
if (!/^[0-9a-f-]{36}$/i.test(idempotencyKey)) {
return res.status(400).json({ error: 'Invalid Idempotency-Key format (UUID required)' });
}
const cacheKey = `idempotency:${req.user?.id}:${idempotencyKey}`;
const existing = await redis.get(cacheKey);
if (existing) {
const record = JSON.parse(existing);
if (record.status === 'in_flight') {
return res.status(409).json({ error: 'Request with this Idempotency-Key is already being processed' });
}
if (record.status === 'completed') {
res.setHeader('Idempotency-Replayed', 'true');
return res.status(record.statusCode).json(record.responseBody);
}
}
// Mark as IN_FLIGHT
await redis.set(cacheKey, JSON.stringify({ status: 'in_flight', createdAt: new Date().toISOString() }), { EX: 300 });
// Intercept response to cache it
const originalJson = res.json.bind(res);
res.json = (body: unknown) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
redis.set(cacheKey, JSON.stringify({ status: 'completed', statusCode: res.statusCode, responseBody: body, createdAt: new Date().toISOString() }), { EX: 86400 })
.catch(err => logger.error({ err }, 'Idempotency cache save failed'));
} else {
redis.del(cacheKey).catch(() => {});
}
return originalJson(body);
};
next();
};
}
Apply to Endpoints
// Order creation: Idempotency-Key required
router.post('/orders', authenticate, idempotencyMiddleware({ required: true }), async (req, res) => {
const order = await createOrder(req.user.id, req.body);
res.status(201).json(order);
});
External API Idempotency (Stripe)
export async function createCheckoutSessionIdempotent(userId: string, items: CartItem[], clientKey: string) {
const stripeIdempotencyKey = `checkout:${userId}:${clientKey}`;
return stripe.checkout.sessions.create(
{ payment_method_types: ['card'], line_items: items.map(toLineItem), mode: 'payment', /* ... */ },
{ idempotencyKey: stripeIdempotencyKey }
);
}
DB-Level Idempotency (INSERT ON CONFLICT)
async function createOrderIdempotent(externalOrderId: string, data: CreateOrderData): Promise<Order> {
try {
return await prisma.order.create({ data: { externalId: externalOrderId, ...data } });
} catch (err) {
if (isPrismaError(err) && err.code === 'P2002') {
return prisma.order.findUniqueOrThrow({ where: { externalId: externalOrderId } });
}
throw err;
}
}
Summary
Design Idempotency with Claude Code:
- CLAUDE.md — required endpoints, 24h TTL, IN_FLIGHT 409 policy
- IN_FLIGHT marker — immediately return 409 for duplicate in-progress requests
- Cache only success responses — allow retries on errors
- External APIs (Stripe) — pass derived Idempotency Key
Review idempotency designs with **Code Review Pack (¥980)* using /code-review at prompt-works.jp*
myouga (@myougatheaxo) — Axolotl VTuber.
Top comments (0)