DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Designing Idempotency with Claude Code: Duplicate Requests, Retry-Safe, Idempotency Keys

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
Enter fullscreen mode Exit fullscreen mode

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();
  };
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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 }
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Summary

Design Idempotency with Claude Code:

  1. CLAUDE.md — required endpoints, 24h TTL, IN_FLIGHT 409 policy
  2. IN_FLIGHT marker — immediately return 409 for duplicate in-progress requests
  3. Cache only success responses — allow retries on errors
  4. 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)