A payments flow in a mobility app creates duplicate charges when the mobile network drops the response and the client retries. Design the idempotency mechanism and identify where teams most often get it wrong.
Show the full answer Hide the answer
The mechanism
The client generates a key once per logical operation — not per HTTP attempt — and reuses it on every retry. The server stores the key with the outcome and returns the stored result for any repeat.
The key must be generated when the user acts (when they confirm payment), persisted locally, and survive the app being killed and restarted. A key regenerated on retry provides no protection whatsoever, and this is the single most common client-side error.
Where teams get it wrong
1. Recording the key and the effect separately. The key must be written in the same transaction as the effect it protects.
- Key written before the effect: a crash between them means the retry is rejected as a duplicate and the payment silently never happens.
- Key written after the effect: a crash between them means the retry creates a second charge.
Only atomic commitment is correct, which usually means the key row lives in the same database as the payment record. This is also why idempotency cannot be implemented at an API gateway — it does not participate in the transaction.
2. Not handling concurrent retries. Aggressive mobile clients retry before the first request completes. Two in-flight requests with the same key must not both proceed. The standard construction is a unique constraint: the first insert wins and proceeds; the second collides and either waits for the outcome or returns a retryable "in progress".
3. Returning a freshly computed response instead of the stored one. A retry must return exactly what the original returned, including the original identifier. Returning a "more current" result breaks the client's model and can hide a duplicate.
4. Ignoring the body. If the same key arrives with a different payload, that is a client bug — possibly a serious one, like a key reused across two different payments. The correct response is an error, not silently returning the old result.
5. Undocumented retention. Keys expire. If a client retries after expiry it will duplicate. That window must be documented rather than hoped about.
6. Idempotency only on the API and not on the internal steps. The charge may cross several internal services. Each must be idempotent on a business key, because at-least-once messaging means every internal step will occasionally run twice.
The mobile-network specific point
On unreliable networks, ambiguous failure is the common case, not the exception. A client cannot distinguish "request never arrived" from "request succeeded and the response was lost". That means:
- Idempotency keys are mandatory on every state-changing operation, not an optional header.
- The client should retry aggressively, because that is safe once idempotency is guaranteed.
- The platform should provide a status lookup by idempotency key, so a client that has exhausted retries can determine what actually happened rather than guessing.
The guarantee to publish
"A retry with the same idempotency key will not create a second charge." Precise, true, and buildable against. Anything implying end-to-end exactly-once across an external payment network is a claim the platform cannot keep.