Idempotency Keys Are Not Optional

The network will retry. Load balancers retry, client libraries retry, impatient users retry. Any write endpoint that is not idempotent is a duplicate-charge, double-order, or corrupted-state incident waiting to happen.
What an idempotency key actually guarantees
An idempotency key is a client-supplied token that says "this is the same logical operation as before." The server's contract: the first request with a given key performs the work; every subsequent request with the same key returns the original result without repeating the work.
A simple, correct implementation
Store the key with the result of the operation, inside the same transaction that performs the write.
INSERT INTO idempotency_keys (key, response, created_at)
VALUES ($1, $2, now())
ON CONFLICT (key) DO NOTHING;
If the insert affects zero rows, the key already exists — return the stored response instead of doing the work again. Because the key and the write commit together, there is no window where one succeeds without the other.
Getting the edges right
- Scope keys to a user or account so one client cannot collide with another.
- Expire keys after a reasonable window (24 hours is common) to bound storage.
- Reject key reuse with a different payload — same key plus different body is a client bug, and returning
409 Conflictsurfaces it early.
Why not just dedupe on natural keys?
Natural keys (order id, transfer reference) work when the client can generate them deterministically. But often the client does not know the id until the server assigns it. An idempotency key sidesteps that — the client generates it up front, before the operation has any identity of its own.
Takeaway
Idempotency is not a nice-to-have you bolt on after an incident. Design it into every write endpoint from day one, commit the key with the work, and retries become boring — which is exactly what you want.
Practical takeaway
Start with ownership and recovery. For every asynchronous boundary, define the source of truth, the retry policy, and the signal that tells an operator the system is drifting.
Context and constraints
Idempotency Keys Are Not Optional is useful because production systems are shaped by competing constraints, not isolated technical choices. Start by naming the users, the operation, the data that must remain correct, and the latency or availability target. Then identify which parts can be asynchronous and which decisions must be visible immediately to a caller.
A good design makes these constraints explicit. It also defines what the system will do when a dependency is slow, unavailable, or returns an ambiguous result. That contract is more valuable than a diagram that only describes the happy path.
A practical design approach
Break the problem into a small number of boundaries with clear ownership. Keep request validation and authorization close to the API edge, keep durable state in one authoritative store, and move retries, reconciliation, and fan-out work into independently observable workers. Each boundary should expose a small state machine rather than leaking implementation details to every caller.
Prefer explicit states such as accepted, processing, completed, and failed over a collection of loosely related booleans. Store the reason for a transition, the actor that caused it, and enough identifiers to trace the operation across services. This makes recovery a normal workflow instead of an emergency script.
Failure modes to design for
The most important failures happen between two successful steps: a request is accepted but the response is lost, a worker completes work but crashes before acknowledging it, or a dependency processes a request and then times out. Treat these as expected states. Use idempotency at every externally visible write, bounded retries with jitter, and a durable record of attempts.
Do not hide all failures behind a generic retry. Classify them as safe to retry, unsafe to retry, or requiring human or reconciliation review. This prevents a degraded dependency from turning a localized incident into a retry storm.
Implementation and observability
Keep the first implementation boring: a narrow interface, parameterized queries, structured errors, and tests around state transitions. Add metrics for request volume, success rate, latency percentiles, retry count, queue age, and unknown outcomes. Logs should carry a stable correlation ID without including secrets or sensitive payloads.
A useful operational dashboard answers three questions quickly: what is failing now, which customers are affected, and whether the system is recovering. Alerts should describe a user-visible symptom and link to the runbook for the next safe action.
Review checklist
Before shipping, verify that the design has an owner for every state, a recovery path for every timeout, a bounded retention policy, and a test for duplicate delivery. Load-test the dependency boundary, exercise partial failures, and document the tradeoff that was intentionally accepted.
The goal is not to eliminate complexity. It is to put complexity behind stable contracts so the system remains understandable when traffic increases or a dependency behaves badly.
Takeaway
Idempotency Keys Are Not Optional becomes production-ready when correctness, failure recovery, and operability are designed together. Start with the invariant, make state transitions durable, keep retries deliberate, and use telemetry to validate the behavior you intended.