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.