A timeout means a response did not arrive in time. It does not establish that an operation never started or that its effect was rolled back. A purchase may have been recorded before a connection closes; repeating it under another identity can create a second purchase.
Idempotency allows an operation to be repeated while preserving its intended effect within a contract. It does not require running the handler only once or always returning the same HTTP status. The central problem is recognizing the same attempt and coordinating which outcome is confirmed, pending, or still unknown.
Define what is being repeated
In an orders API, an identity can combine the authenticated actor, operation type, and Idempotency-Key. The key identifies an attempt; it neither replaces authorization nor identifies a trustworthy user on its own. The API Design contract still validates permissions and inputs on every request.
A fingerprint of the accepted parameters is retained. Its canonicalization must be defined: relevant fields, currency, quantities, and contract version, without accidentally depending on JSON property order. Reusing a key with an incompatible representation produces a conflict rather than silently changing the first attempt. A new key can still violate a business rule: one order per hold needs its own independent constraint.
Effect and record inside a local boundary
For an effect within an authoritative database, the deduplication record and business change commit in the same transaction. An earlier lookup that finds no key is insufficient: two concurrent requests can both observe its absence.
| Durable data | Purpose |
|---|---|
| Actor, operation, and key | A unique constraint with non-null fields recognizes an attempt. |
| Parameter fingerprint | Detect incompatible reuse. |
| State and result | Distinguish pending, completed, and definitive failure. |
| Business identity | Apply invariants that must survive new keys. |
In PostgreSQL, inserting under a unique constraint coordinates the race. The application must resolve a conflict, read confirmed state, and compare the fingerprint; ON CONFLICT does not make these decisions automatically. A competitor can wait for the preceding transaction. Waiting and transaction duration need bounds, with recovery for an uncertain outcome.
In the local case, creating the record, making the change, and storing the result can belong to one short transaction. If asynchronous processing is needed, durably accepting pending state is another contract; it requires later recovery and does not mean the effect has completed.
A flow with observable outcomes
The core of the diagram represents a local effect. Concurrent execution has a bounded wait; querying an already accepted asynchronous pending operation is a different contract. Calls to another store or provider have another boundary.
--- config: look: handDrawn --- flowchart TD client["Request<br/>Actor + key + parameters"] --> identity["Validate identity<br/>and attempt fingerprint"] identity --> record subgraph local["Transaction"] record["Resolve unique record<br/>and attempt fingerprint"] -->|"New"| effect["Record + effect + result<br/>Same local commit"] end effect --> result["Return confirmed result"] record -->|"Compatible completed attempt"| result record -->|"Different parameters"| conflict["Conflict"] record -->|"Local execution in progress"| wait["Bounded wait<br/>Resolve commit or abort"] wait -->|"Resolved outcome"| result record -->|"Durable asynchronous pending state"| recovery["Query asynchronous state<br/>or recover the attempt"] recovery -->|"Resolved outcome"| result
A response lost after commit allows the durable result to be returned on retry. If the transaction aborts, no separately confirmed effect remains without its record. This protects that local boundary, without promising a single execution of code, notifications, or external services.
External effects and unknown outcomes
A charge and a PostgreSQL write do not become atomic by sharing a key. The attempt is recorded, execution ownership is coordinated, and the identity recognized by the provider is retained. The network call stays outside a long transaction, but moving it outside the lock does not solve its safety.
If the provider supports idempotency, its scope, fingerprint, and retention window matter. After a timeout, the same attempt is queried or retried under that contract. Outside the window, creating another attempt can duplicate a charge: reconcile first. The ticketing system retains an order and coordinates its attempts, including late outcomes and notifications.
Locally cancelling a task does not establish that the provider cancelled its effect. State changes must coordinate execution, cancellation, and reconciliation so that incompatible decisions cannot commit simultaneously.
Temporary ownership and retention
Pending state needs a recovery policy if its executor disappears. A lease allows renewal or reclamation according to a deadline, but expiry does not necessarily stop the previous owner. Ownership versions and conditions at commit can reject old writes; an external effect needs protection at its own boundary.
Key retention also limits the guarantee. If a result is deleted after twenty-four hours, a later replay may look new. That period is an example decision rather than a universal recommendation. Permanent order identity, business constraints, or durable history can provide additional protection. Cleanup considers privacy, storage, and the actual retry window.
Consumers and publication
Message Queues consumers recognize events by stable identity and coordinate deduplication with their local effect. Acknowledging after commit allows another delivery if the ack is lost; the record prevents repeating that change. Acknowledging beforehand can lose work.
An outbox retains the event with the local change, but its relay can publish more than once. Ordering and deduplication are separate contracts: preventing duplicates does not prevent applying an old version over a newer one. Event identity and entity sequence answer different questions.
Verify the contract
Tests send two equal requests concurrently, change the payload while keeping the key, and lose the response after commit. They also stop an executor after an external effect, let its lease expire, and deliver a late response. The expected result is an explainable durable decision rather than merely an absence of HTTP errors.
Fingerprint conflicts, pending attempts, unknown-outcome age, reconciliations, and redundant effects are observed. A high retry rate can be safe for the data while costly for capacity; limiting its budget remains necessary. Correctness is demonstrated within the stated boundaries and deadlines.
References: HTTP idempotent semantics, RFC 9110 and safe retries with idempotent APIs.