Idempotency Is Easy Until the Second Request Is Different

Idempotency is more than just replaying responses; it's about handling concurrency, partial failures, and inconsistent request bodies. This article explores why a simple replay cache isn't enough for robust API design.
People talk about idempotency like it is a solved problem:
Put an Idempotency-Key on the request. Store the response. Replay it on retry.
And yes, that is doable. For the happy path, it is even fairly small.
The client sends:
POST /payments
Idempotency-Key: abc-123
Content-Type: application/json
{
"accountId": "acc_1",
"amount": "10.00",
"currency": "EUR",
"merchantReference": "invoice-7781"
}
The server checks whether it has seen abc-123. If not, it creates the payment. If yes, it returns the previous response.
That version survives the demo. The part I contest is that this is the hard part. It is not. The hard part starts with the second request, because the second request is not always a clean replay of the first one.
Maybe it is a completed replay. Fine. Return the stored result. Maybe it arrives while the first request is still running. Now your idempotency layer is part of your concurrency control. Maybe the first request created a local payment but crashed before publishing an event. Now the local row and the external side effects are out of step.
Or maybe the second request has the same key and different content. This is the case that makes idempotency interesting. Is it a retry? Is it a client bug? Is it a new operation? Should the server replay the old response, reject the request, or treat (key + content) as a new identity?
My bias for side-effecting APIs is: same scoped key plus different canonical command should be a hard error. It catches client bugs early. A client that believes it is safely retrying a 10 EUR payment should not have the server silently interpret the second request as something else.
Idempotency is about the effect. An operation is idempotent if applying it once or many times has the same intended effect. If your design only handles completed same-command retries, it is a replay cache. That might be enough for some endpoints. But it is not the whole problem.
Source: Hacker News
















