Assume the network will lie
A client can send a request successfully while receiving a timeout. The user retries, the worker retries, or a load balancer retries. Any write API that assumes one request equals one attempt is eventually going to create a duplicate.
An idempotency key gives the operation an identity that survives transport retries. The server stores the result alongside the key and returns the same outcome for a repeated request.
The details matter
Keys need an owner, an expiry policy, and a payload consistency check. Reusing a key for a different body should be an error, not a lucky overwrite.
The database transaction must cover both the business effect and the idempotency record. Otherwise the system can remember a request without applying it, or apply it without remembering it.
Reliable APIs make retries boring. That is the point.
Two layers of protection
An idempotency key protects the application operation. A database constraint protects the business invariant. The key prevents the same request from being applied twice; the constraint prevents two different requests from creating an impossible state.
sequenceDiagram
participant C as Client
participant A as API
participant D as Database
C->>A: POST with Idempotency-Key
A->>D: Begin transaction
D-->>A: No prior key
A->>D: Apply effect and store result
D-->>A: Commit
A-->>C: Result
C->>A: Retry with same key
A->>D: Find stored result
D-->>A: Original result
A-->>C: Same result
What to store
For each key, I store the authenticated actor, a request fingerprint, the final status, and the response body or a durable reference to it. A key reused with a different payload should return a conflict. Retaining only a boolean is not enough because the client needs the original result after a timeout.
const result = await database.transaction(async (tx) => {
const existing = await tx.idempotency.find(key);
if (existing) return existing.response;
const created = await tx.orders.create(input);
return tx.idempotency.store(key, {
requestHash: hash(input),
response: created,
});
});
Expiration and concurrency
Keys need an expiry window that matches the operation. A payment may need longer protection than a UI preference update. Concurrent requests with the same key must contend on a unique database index or lock; checking for an existing key and inserting later leaves a race.
Idempotency is not a license to make every endpoint repeatable. It is a precise contract that says what repeated intent means for one operation.
