Reuse an operation identifier across retries.
Reject a reused key with a different intended operation.
Plan for a crash between an external effect and its recorded response.
Start with an ambiguous outcome
A customer presses “place order.” Your server writes order 740, but the connection closes before the response reaches the browser. The browser knows that it did not receive confirmation. It does not know whether the order exists. Treating every timeout as a failed operation turns a recovery attempt into a second purchase.
Use an explicit operation identity. In this original example, client operation k-17 means “create this particular order.” Keep that identity when retrying the same intent. Another intentional purchase receives another identity, even if its contents happen to match.
| Arrival | Operation key | Intended order | Expected decision |
|---|---|---|---|
| First | k-17 | basket A | Reserve and execute |
| Retry | k-17 | basket A | Return recorded result or pending status |
| Mistake | k-17 | basket B | Reject parameter mismatch |
| New purchase | k-18 | basket A | Treat as a new operation |
Scope the identity and compare the meaning
An idempotency key is not authentication. Bind it to the authenticated account and operation type. Otherwise one caller might collide with another caller's key or retrieve a response belonging to someone else. Do not put private customer details in the identifier.
Store a representation of the operation's meaningful parameters. Normalize only according to your contract: JSON property order may be irrelevant, but amount, currency, destination and item quantities are usually meaningful. The same key paired with a changed operation should produce an explicit conflict, rather than silently returning an unrelated earlier result.
Reserve before executing
Imagine a ledger with account, operation type, key, parameter fingerprint, state and result identifier. A uniqueness constraint or transaction must decide which concurrent request owns the first execution. Two requests that both read “not found” must not both proceed independently.
Possible states include reserved, completed and reconciliation_required. A completed record can return its stable result. A reserved record needs a documented pending or retry response. Expiry needs a deliberate policy: forgetting a key too soon can allow a delayed retry to repeat the operation.
Test the difficult failure boundary
A local transaction can keep an order and its local ledger consistent. It cannot automatically make a remote provider call part of the same transaction. If the remote effect succeeds and your process crashes before recording it, blindly repeating the call can still duplicate work.
Use the remote provider's supported identity mechanism when available, retain its operation identifier, and reconcile ambiguous outcomes. Provider retention periods and error semantics differ; consult the actual API rather than copying one vendor's rules into every integration.
Test simultaneous duplicates, changed parameters, failure before execution, failure after commit, a delayed retry after expiry and a provider response that was lost. Record both the number of effects and the returned state. A successful HTTP response alone does not prove that the system created exactly one business effect.
Make the contract explainable
Write down what “same operation” means, how long the guarantee lasts, and what a caller should do while an outcome is uncertain. Those three answers are more useful than an unqualified promise that retries are always safe.
Check your understanding
A request times out after an order was committed. Should its retry use a fresh key?
Reveal the reasoning
No. For a retry of the same intended operation, use the same correctly scoped key and matching parameters. A new key describes another operation and can create a duplicate.
Sources & further reading
Created with AI assistance. Examples are educational; verify platform-specific behavior against the linked documentation. Found something unclear? Contact us.