*Idempotence

September 15, 2025

Why idempotence?

Retries are inevitable (timeouts, mobile networks, crashes). Without idempotence, a retry can double‑charge a card or create duplicate orders.

What idempotence guarantees

Executing the same operation multiple times with the same intent produces the same end state (and a predictable response).

Patterns

  • Idempotency keys: client generates a stable key per intent; server stores the result keyed by that id.
  • Upsert/MERGE: perform create‑or‑update atomically.
  • Natural idempotence: PUT /resource/:id sets a full state; re‑sending yields same result.
  • Sequence numbers: only accept strictly increasing sequence for a given stream.

Code: HTTP idempotency key

# language-http
POST /payments
Idempotency-Key: 4a9f7b76-9b1d-4f3f-9a8d-0e6e32
Content-Type: application/json
{"amount": 5000, "currency": "USD", "source": "tok_123"}
// language-typescript
// Store outcome by key with TTL; return same response on repeat calls
async function createPayment(req, res) {
  const key = req.get('Idempotency-Key')
  if (!key) return res.status(400).json({ error: 'missing idempotency key' })
  const cached = await redis.get(`idem:${key}`)
  if (cached) return res.json(JSON.parse(cached))
  const result = await chargeCard(req.body) // side effect
  await redis.set(`idem:${key}`, JSON.stringify(result), 'EX', 24 * 3600)
  return res.json(result)
}

Code: SQL upsert

-- language-sql
INSERT INTO orders(id, user_id, total_cents, status)
VALUES ($1, $2, $3, 'PAID')
ON CONFLICT (id)
DO UPDATE SET status = EXCLUDED.status, total_cents = EXCLUDED.total_cents;

Consumer‑side dedup (at‑least‑once)

// language-typescript
async function handleEvent(evt) {
  const seen = await redis.setnx(`evt:${evt.id}`, 1)
  if (!seen) return // already processed
  await process(evt)
}

Exactly‑once: what’s realistic

  • True exactly‑once across distributed boundaries is rare/expensive.
  • Emulate it: transactional outbox + idempotent consumer; or keep consumer offsets and state in one transaction.

Response replay

  • Cache full response per idempotency key to return the same payload/status on retries.
  • Include the same resource ID so clients do not create duplicates.

Choosing keys

  • Derive from business intent: orderId, clientToken, or a UUID stored before attempting the side effect.
  • Scope keys to endpoint + user to avoid collisions.

Edge cases

  • Time window: store keys long enough to cover client retry windows.
  • Partial failures after side effect: design operations to be reversible or compensatable.

Analogy

Like a coat check ticket: even if you ask twice with the same ticket, you get the same coat—not two coats.

FAQ

  • Do I need idempotency for GET? GET must already be safe; idempotency is vital for POST/side‑effecting operations.
  • Is PUT inherently idempotent? Yes by spec, but your handler must implement it correctly.

Try it

Write a test that sends the same POST with the same Idempotency-Key 10 times under network timeouts and assert a single charge/order is created and the same response is returned.