Idempotency Key Scoping
also called Key Scope, Deduplication Scope
The decision of what an idempotency key is unique within, what is stored alongside it, and how long it lives - the three choices that determine whether deduplication actually works.
An idempotency key makes a repeated request return the original result instead of performing the operation again. That description is easy and the implementation is not, because three separate decisions determine whether it works, and each has a characteristic way of failing silently.
Scope: what is the key unique within? Content: what is stored with it? Lifetime: how long does it live?
Why it matters
On a payment path, retries are not an edge case. Mobile clients on unreliable networks cannot distinguish a lost request from a lost response, so they must retry, and during a commerce surge retries are a substantial share of total load. Deduplication is therefore load-bearing infrastructure, not a safety net.
The failures are silent by nature: a duplicate payment looks like a payment, and a wrongly-deduplicated request looks like a success.
Implementation patterns
- Scope to (tenant, key). Global uniqueness lets one merchant collide with another's key space; per-request uniqueness is not uniqueness at all. The tenant is almost always the right boundary.
- Store a fingerprint of the request — a hash of the material parameters — alongside the key. If a client reuses a key with different parameters, that is a client bug, and returning the previous response silently is the worst available behaviour. Return a conflict.
- Commit the key record in the same transaction as the state change. Key first then payment makes a genuine request look like a duplicate forever after a crash. Payment first then key produces a duplicate on retry. One transaction, or the mechanism does not hold.
- Set the lifetime longer than the longest client retry window, including a mobile client that retries the next morning after being backgrounded.
- Design the in-flight case explicitly: the key exists, the response does not, because the first request is still running. Answering "duplicate" is wrong and answering "success" is a lie. A distinct in-progress response with a retry-after is the only correct answer, and under surge it stops being rare.
Industry example
Payment platforms such as Razorpay and PhonePe expose idempotency keys as a first-class part of the merchant API, and the interesting part is what surrounds the key rather than the key itself. The ordering rule that matters is that the attempt is persisted with its key before the downstream provider is called, so a crash mid-call leaves a record to reconcile from rather than a payment that exists at the provider and nowhere else. That single ordering choice is the origin of most reconciliation breaks in payment systems.
The second lesson from that domain is coverage: teams key the payment-creation endpoint and forget refunds, captures, cancellations and inbound webhooks — the lower-volume flows with higher financial consequence per error.
Failure scenarios
- Key stored after the effect, producing duplicates on retry.
- Key stored before the effect, producing permanent false duplicates after a crash.
- No fingerprint, so a reused key silently returns the wrong payment's response.
- TTL shorter than the retry window, so a late retry looks like a fresh request.
- Coverage gaps on refunds and webhooks.
- In-flight case unhandled, so concurrent retries either duplicate or falsely deduplicate.
Trade-offs
The idempotency record is a write on the hot path and a table that grows quickly, which is a real cost at payment volumes. It also requires a storage system with transactional guarantees alongside the state change, which constrains the database choice for that service.
The alternative — relying on the downstream provider's deduplication — delegates your correctness to someone else's implementation and does nothing about duplicates created inside your own system before the provider is reached. The cost is worth paying, and the design question is only how to keep the table bounded, usually with a TTL plus an archive.
Interview question
"A merchant reports being charged twice. Your API accepts idempotency keys and the merchant swears they sent the same key. Walk me through every place in your system where a duplicate could still have been created."