A delivery app marks a parcel delivered. The phone is on a weak connection: the request reaches the server, the response is lost, and the app retries. The customer is charged twice and the parcel is marked delivered twice. Which change fixes this properly?
Show the full answer Hide the answer
The mechanism
A lost response is indistinguishable from a lost request. The client knows only that it did not hear back, and both possibilities — the server did the work, or it never saw the request — require different actions. Since the client cannot tell, the network gives you at-least-once delivery, and the only place the ambiguity can be resolved is the server, because the server is the one that knows whether it already acted.
The key must be generated by the client, before the first attempt, and reused on every retry. That is what makes the retry recognisable as the same action rather than a new one. The server stores the key with the outcome and, on seeing it again, returns the stored result without repeating the effect. Payment APIs have used this shape for years — Stripe's idempotency keys are the widely documented example — and the pattern is identical for any write a flaky client can repeat.
Two details decide whether it works in practice: the key must survive an app restart, so it is written to local storage with the queued action rather than held in memory, and storing the key and performing the effect must be atomic, or a crash between them recreates the double-charge you were preventing.
Why the other options fail
- Server-side dedup by parcel within five minutes. This is the right instinct applied with the wrong identity. It works only if the action is naturally unique per entity and never legitimately repeats, so it protects "mark delivered" and silently breaks "add a 5 kg surcharge", where two identical charges can both be real. It also picks an arbitrary window: retries from a queued offline action can arrive hours later.
- A longer client timeout. A longer wait reduces how often you retry without the response arriving; it cannot remove the case. It also makes the app feel broken, and on mobile the radio may be gone entirely, so no timeout is long enough.
- Check whether the parcel is already delivered, then retry. Two requests where there was one, and the check-then-act pair is a race: two retries can both read "not delivered" and both proceed. It also cannot answer the question for actions that are legitimately repeatable.
When this is unnecessary
A request that only reads, or a write whose effect is naturally idempotent — setting a field to a value rather than incrementing it — needs none of this. The discipline is for actions that create something, move money, or append to a ledger. Modelling a write as "set this state" instead of "apply this delta" is often the cheapest way to make the problem disappear entirely.