Design retry safe create endpoints with idempotency keys

A customer submits a reservation. The database commits it, but the connection closes before the response reaches the browser. The user retries and receives a second reservation. A disabled submit button helps with double clicks, but it cannot resolve a lost response after the first request reached the server. The server needs a way to recognize the same intended operation.
This example uses a Node API and MongoDB. It assumes the local reservation and its idempotency record can participate in the same transaction on a supported deployment. External payment or email effects require additional contracts; a local transaction does not make an external provider action exactly once.
Define the identity of a retry
An idempotency key is a caller-supplied opaque value reused for retries of one operation. Scope it by authenticated actor or tenant and operation type. A global key alone can allow one caller to collide with another caller's request. Validate length and accepted representation, and never treat possession of a key as authorization.
Store a fingerprint of the validated semantic payload. If the caller reuses the key with a different reservation, return a documented conflict rather than replaying an unrelated result. Decide which fields affect the fingerprint and normalize them consistently. Raw JSON text is often a poor fingerprint because property order or irrelevant formatting can differ.
Define retention. A key remembered for one day provides a different retry guarantee from a key remembered indefinitely. Expiry must reflect the business operation and client retry window. A database TTL cleanup is not an exact wall-clock deletion guarantee, so do not promise precise expiry based solely on background deletion.
Model the state explicitly
A durable record can include scope, key, payload fingerprint, operation identifier, status, and safe response data. Useful states include processing, completed, and a deliberately recorded failure outcome. Decide whether a transient dependency failure releases the key for retry or preserves a recoverable operation.
Concurrent requests with the same key need one authoritative claim. Enforce scope and key uniqueness in storage. A read followed by an insert can race just like the duplicate invitation example in concurrent uniqueness.
Do not leave processing records stuck forever without a recovery policy. A process can crash after claiming a key. A lease, transaction boundary, or reconciliation procedure may be needed depending on where work occurs. A timer that blindly lets another worker proceed can duplicate an external side effect still running elsewhere.
Keep local persistence atomic where possible
For a purely local reservation, one transaction can create the reservation and the completed idempotency result together. A unique key conflict identifies a concurrent or earlier request. The caller can then read the stored record, compare the fingerprint, and replay the result if completed.
An illustrative operation sequence is: validate request, authenticate scope, derive fingerprint, start transaction, insert scoped key, create reservation, store response identity, and commit. Keep the transaction short and avoid network provider calls inside it. Driver transaction helpers may retry callbacks, so callback code must tolerate re-execution.
Store only response fields safe for this authorized caller. A replay must still enforce identity and access. Do not persist a raw response containing secrets or transient internal errors just because it is convenient to return the same bytes later. Decide which headers and status belong to the replay contract.
Handle external effects honestly
If reservation creation also charges a provider, use that provider's documented idempotency mechanism when available and persist the relationship to the local operation. An outbox or background workflow can separate durable local intent from delivery. Each boundary still needs its own duplicate and recovery rules.
A request timeout means the outcome may be unknown, not necessarily failed. Query the durable operation state or reconcile with the provider before issuing another charge. A local key cannot prove what an external service did if the response was lost.
Expose a status endpoint when long processing makes immediate replay impossible. The client can show processing and poll with bounded behavior instead of asking users to create a new operation. Avoid a misleading success banner when only the local intent has been stored. Third-party failures covers those states.
Verify retries and concurrency
Use a synthetic reservation and send the same key twice with the same payload. Assert one reservation exists and both responses identify it. Send the same key with a different payload and assert the documented conflict. Send the same key from another tenant and verify it cannot read or overwrite the first tenant's result.
Create simultaneous requests using a test barrier around the claim step. Assert storage uniqueness and response behavior. Then inject a failure after the database commit but before the HTTP response. The retry should recover the completed result rather than create a new reservation.
Test a crash before completion and exercise the recovery policy. Test retention boundaries and a legitimate new operation with a new key. A passing duplicate-call unit test does not establish that crash recovery works. Use isolated real storage for transaction and uniqueness claims, with provider doubles for application state transitions.
Ask AI to enumerate ambiguous windows
Provide the operation sequence, transaction boundary, provider contract, and state machine. Ask an assistant what happens if the process stops after each step. Require it to distinguish local atomic guarantees from external uncertainty. This produces a concrete failure matrix to review.
Challenge suggestions that promise exactly once because a key exists. Keys are a mechanism for recognizing repeats within a defined scope and retention period. They require durable state, concurrency control, and recovery. The assistant should identify how each of those requirements is implemented, not simply add a header to fetch.
Keep credentials and real customer operations outside the prompt. Synthetic payloads and redacted provider responses can exercise the same logic. The verification record should show which windows were tested and which depend on the provider's documented behavior.
Keep complexity proportional to consequences
A low-impact draft creation endpoint might use a simple unique client operation identifier stored on the record. A payment or reservation workflow needs stronger replay and reconciliation semantics. Choose from the consequence of duplication and the expected retry behavior rather than applying the most elaborate pattern everywhere.
Document the client's responsibility to reuse the key for the same operation and generate a new one for a new operation. A client that generates a fresh key on every retry defeats the guarantee. A client that reuses one key forever blocks legitimate operations.
Transaction decisions helps establish the local atomic boundary. With that boundary and an explicit external recovery policy, a create endpoint can respond honestly to retries even when the network hides the first result.
Sources
About the Author

Kamruz
Full Stack Developer
A passionate developer who loves creating modern web applications and sharing knowledge with the community.
More About Me