Idempotency Key Interview Questions: Request Fingerprints, Expiration, and Concurrent Retries
Quick Overview
Define an idempotency contract with scoped keys, validated request fingerprints, atomic ownership, replay retention, and tested concurrent-retry failure cases.
Several clients retry POST /reservations with the same idempotency key. One changes the quantity, one arrives while the first request is running, and another returns after the stored record expires. Saying “cache the response” leaves all three cases unanswered.
The answer to idempotency key interview questions is a contract: scope the key to a caller and operation, bind it to a validated request fingerprint, claim it atomically, and define what retries observe before completion and after expiration. A hash detects incompatible reuse; it does not prevent two workers from executing.
Practice that contract with System Design questions on PracHub. Below, one reservation API connects fingerprints, concurrent ownership, crash recovery, and retention.
Evidence boundary: provider behavior is labeled as official fact. Our reservation contract, failure examples, and preparation advice are design inferences. We found no verified firsthand candidate report establishing that these exact exercises appeared in an employer's interview. The local checks exercise Python and SQLite, not live payment APIs or a production distributed deployment.

What is the difference between a key and a fingerprint?
A client-generated key identifies one intended operation across delivery attempts. A fingerprint represents the request's relevant meaning. Keep both: identical parameters can describe two legitimate reservations, while a reused key can accidentally carry different parameters.
Our example namespaces records by (tenant_id, operation, key). The authenticated session supplies the tenant; the operation is create_reservation. A new reservation gets a new key, even if its SKU and quantity match yesterday's order. A retry preserves its original key.
Official guidance: AWS describes caller-provided request identifiers and cautions that inferring duplicates from identical parameters can conflate distinct intentions. It also requires atomicity between recording the identifier and related mutations. These are service-design considerations, not a guarantee supplied by a header. AWS retry guidance
Authorization remains independent. Knowing another tenant's key must not expose its stored response. Recheck access before returning a replay, and avoid logging sensitive payloads as convenient debugging identifiers.
What belongs in a request fingerprint?
Define equivalence before choosing SHA-256. For our fixed-schema API, the relevant fields are SKU, integer quantity, and warehouse. Omitted warehouse means main; an explicit null is invalid. Unknown fields are rejected, and whitespace inside an identifier is significant.
The following original helper receives an already parsed JSON object. It deliberately supports only nonempty ASCII identifiers and positive integer quantities. The HTTP layer must separately reject duplicate JSON member names and enforce body-size limits.
import hashlib
import json
def fingerprint(body):
allowed = {"sku", "quantity", "warehouse"}
if not isinstance(body, dict) or set(body) - allowed:
raise ValueError("invalid fields")
sku = body.get("sku")
quantity = body.get("quantity")
warehouse = body.get("warehouse", "main")
for value in (sku, warehouse):
if not isinstance(value, str) or not value or not value.isascii():
raise ValueError("invalid identifier")
if type(quantity) is not int or quantity <= 0:
raise ValueError("invalid quantity")
normalized = [1, "POST", "/reservations", sku, quantity, warehouse]
encoded = json.dumps(normalized, separators=(",", ":"))
return hashlib.sha256(encoded.encode("ascii")).hexdigest()
Changing object-member order leaves the fingerprint unchanged because fields are extracted explicitly. Adding warehouse: "main" also preserves it under our declared default. Changing quantity changes it. Boolean true, string "2", and floating-point 2.0 are rejected rather than silently becoming integer 2.
Official specification: RFC 8785 defines JSON canonicalization, including deterministic property ordering and serialization rules. It does not decide your API's business equivalence. Our restricted helper is not a general RFC 8785 implementation. JSON Canonicalization Scheme
For a richer API, include every field that affects the operation, including relevant query parameters and API semantics. Do not sort arrays unless their order is explicitly irrelevant. Store the fingerprint algorithm version so a deployment does not reinterpret retained requests under a changed normalization rule. Compare existing keys using their stored scheme, or reject incompatible API versions explicitly.
What should a repeated request receive?
Choose observable outcomes before choosing Redis, PostgreSQL, or a framework. Our example uses the following contract; these status codes are design choices, not universal provider requirements.
| Stored record in the same namespace | Result for this request |
|---|---|
| No record; valid authorized input | Compete for ownership; execute only after winning |
| Active or retained key, different fingerprint | 409 with idempotency_mismatch; no execution |
| Same fingerprint, work in progress | Bounded wait or 409 with operation_in_progress |
| Same fingerprint, completed within retention | Replay stored status, response body, and selected stable headers |
| Outcome unresolved | Return an explicit pending/unknown result; reconcile |
| Completed record logically expired | Follow the documented post-expiration policy |
Mismatch takes priority over a generic “still processing” response. Otherwise, a client can keep retrying the wrong request without learning that its operation identity is inconsistent.
Do not treat validation rejection as successful execution. Decide whether invalid attempts reserve keys at all. Our contract validates before claiming, so a corrected request may reuse the unclaimed key. Once a valid request owns the key, changing its fingerprint is a conflict.
How do concurrent retries elect one executor?
A check followed by an ordinary insert is unsafe: workers A and B can both read “absent” before either writes. The storage system must choose one owner, typically through a unique constraint and a transactional insert or conditional write.
For a short operation entirely inside one database, our design puts the key claim, reservation insert, and stored response in one transaction. The unique key covers the complete namespace. A competing request waits for the transaction outcome, then reads the committed record and compares fingerprints.
Official fact: PostgreSQL documents that unique-index conflicts can block concurrent inserts; ON CONFLICT provides an alternative to raising a uniqueness error. That mechanism handles an insertion conflict, not the whole application workflow. PostgreSQL INSERT
With a bounded wait, a duplicate may receive an in-progress response rather than waiting indefinitely. Distinguish this from the synchronous path that eventually replays a committed result. When a conflicting insert returns no row, read the winning record; do not assume the original operation failed.
Avoid overwriting a completed record with the latest caller's fingerprint. That converts a misuse detector into a silent second-operation mechanism. Also explain cross-region routing: independently writable stores can each elect a winner unless ownership has an authoritative coordination boundary.
What happens if the server crashes after creating the reservation?
Draw the transaction boundary. If the reservation and response record are in the same database transaction, a crash before commit leaves neither committed. A retry may execute. If commit succeeds but delivery fails, a retry reads the stored response without creating another reservation.
Our local SQLite fixture tests rollback before commit and replay after a lost response, using separate connections for competing requests and reopening storage for replay. SQLite's writer serialization is not a PostgreSQL benchmark; it makes the narrow transactional invariant executable.
The answer changes when an external service performs the effect. Your database cannot roll back a remote shipment by rolling back its local transaction. Persist an operation identity, propagate a stable downstream idempotency key where supported, and reconcile an ambiguous response against authoritative downstream state.
An outbox can atomically record a local mutation and delivery intent. It does not make the external receiver process a delivered message only once. The receiver still needs deduplication or an operation whose repeated application is safe. Avoid labeling an unknown remote result as FAILED merely because your request timed out.
Is key expiration the same as a worker lease?
No. A worker lease limits ownership in time; replay retention limits how long the service remembers an operation. Expiring ownership does not prove that a paused worker has stopped, nor that its remote request never committed.

Consider this deliberately broken schedule: A claims K and pauses before its effect. At 10:00, cleanup deletes K because a timer elapsed. At 10:01, B claims K and performs the reservation. At 10:02, A resumes and performs it again. Cleanup erased coordination state while work remained unresolved.
Our design inference is to expire terminal replay records separately from active or unknown operations. Recovery of abandoned work needs an explicit ownership protocol. A monotonically increasing generation can fence a stale worker only if the protected write checks it atomically; a token in a log provides no protection.
For external effects, a local ownership check can race with dispatch. The downstream service must enforce the corresponding identity or fencing contract, or recovery must preserve uncertainty. Deleting the local row cannot settle the remote outcome.
How long should records remain, and what happens at the boundary?
Start with the longest supported retry horizon: client retries, queued delivery, offline recovery, and any permitted manual replay. Add operational margin and define the timestamp from which retention starts. Our exercise retains completed results for ten minutes after commit solely to make boundary tests short; this is not a production recommendation.
We define now < expires_at as replayable and now >= expires_at as logically expired. The chosen contract permits a new operation after terminal-record expiration. Physical cleanup may run later, so request handling must apply the same logical rule while atomically replacing an expired record.
That choice has a cost: an old retry can produce another reservation. If the business forbids duplicates beyond this window, use a longer-lived business operation ID or uniqueness constraint. Keeping a compact tombstone can reject old keys after discarding large response bodies, but its own retention and deletion rules still need definition.
Use server-controlled timestamps and avoid letting every retry extend expiry indefinitely unless that behavior is intentional. Test the exact boundary, not only a comfortably early replay. Document whether clients must reconcile an old operation before submitting a new one.
Do Stripe and PayPal return the same kind of replay?
Official fact: Stripe saves the first executed request's status and body, including 500 responses. It compares reused-key parameters, allows pruning after keys are at least 24 hours old, and treats reuse after pruning as a new request. Validation failures and conflicts with concurrently executing requests do not save an idempotent result. Stripe idempotent requests
Official fact: PayPal documents replay through PayPal-Request-Id as returning the previous request's latest status. Support and retention depend on the API. For simultaneous requests with the same ID, it processes the first and might fail the second. PayPal idempotency
These contracts demonstrate why “always return the original bytes” is too broad. Ask whether your endpoint replays a stored response, returns current operation status, or rejects concurrent duplicates. Also distinguish an application-level stored failure from a transport failure whose remote outcome is unknown.
Which tests should you defend in an interview?
Use assertions about effects and identity, not just response codes. Our 32 passing local checks compare fingerprints for reordered members and explicit defaults, reject changed intent and invalid types, and run two connections against the same key. They also check rollback before commit, replay after a lost response, and immediately-before/at/after expiration.
A separate deterministic race model reproduces duplicate effects after premature cleanup and rejects a stale generation when the protected write enforces it. These tests establish the modeled contract; they do not certify multi-region durability or a payment provider integration.
The following PracHub records provide related practice, not predictions of your exact interview. Add one key-reuse or expiry follow-up to each.
| PracHub question | Practice focus |
|---|---|
| Prevent Duplicate Request Processing | Define the operation identity and one-executor invariant. |
| Design and Implement an Idempotency Layer for Order Creation | Specify mismatch, in-flight, and stored-response behavior. |
| Execute a TTL Key-Value Store with Transactions | Test logical expiry and transaction boundaries. |
| Design an async API with idempotency | Separate acceptance, execution, and terminal status. |
| Implement an Idempotent Versioned Database Update | Combine replay identity with stale-write protection. |
Use the distributed-systems interview guide for broader consistency tradeoffs. Then choose a System Design exercise and explain who owns K, what its fingerprint means, and exactly when its protection ends.
Sources and Further Reading
- Stripe: Idempotent requests
- PayPal: Idempotency
- AWS Builders' Library: Making retries safe with idempotent APIs
- PostgreSQL: INSERT and ON CONFLICT
- RFC 8785: JSON Canonicalization Scheme
Checked September 21, 2026. Provider-specific contracts can change. The reservation API is an original exercise with explicit assumptions.
Comments (0)