TATECHATLAS
◎ English
Web & APIs

Duplicate API submissions: design an idempotency contract before adding a key

Define the caller, payload, atomic claim and replay policy so a retry has a predictable application result.

On this page

For a retried order submission, reuse a key for the same authenticated caller and the same logical payload. The application can atomically claim that combination and retain its completed result for replay. A changed payload using the same key should receive a documented application conflict. A unique database row coordinates claims; it does not alone guarantee that a payment, email or other external effect occurs exactly once.

Separate HTTP semantics from the application contract

An idempotent operation has the same intended server effect when repeated as when performed once. Responses need not be identical for that definition to hold. A POST endpoint does not acquire a safe retry contract merely because a client sends an extra header. The server must implement and document how it interprets that key, which operations it covers, and how retries interact with authentication and stored state.

Scope the key to a trusted caller

In the hypothetical example, the key k1 belongs to one authenticated caller and one order-creation operation. Another caller using k1 must not receive the first caller's result. Obtain the caller identity from trusted authentication rather than from a freely supplied payload field. Define whether keys share a namespace across operations or are scoped to a particular endpoint, and preserve that scope in the database uniqueness rule.

Specify payload equivalence

The same key should represent the same logical submission. Define which fields are part of the operation and how the application compares them, for example using a canonical representation or a digest under a documented rule. Raw JSON bytes may differ while representing the same data, so byte equality is a policy choice. Conversely, excluding an important field such as amount can incorrectly identify different orders as equivalent.

Walk through the constructed retry

Suppose caller A submits key k1 with a payload requesting two units of item X. The first successful request stores an order result. A retry by A with k1 and the equivalent payload returns that retained result under this proposed contract. A request using k1 but requesting three units is rejected as a payload mismatch. This is a design illustration, not a claim that every existing API uses the same status code or replay behavior.

Claim the key atomically

A check-then-insert sequence can race: two requests can both see no existing key. A database uniqueness constraint over the chosen caller, operation and key scope can enforce a single stored claim. PostgreSQL INSERT ON CONFLICT can help coordinate the insertion. The application still needs to interpret whether it owns a new claim or has found an existing one, and must not let every losing request perform the protected operation anyway.

Represent processing and completed states

Store enough state to distinguish a request that is still being processed from one whose result can be replayed. Define how a concurrent retry responds while the first attempt is running: waiting, a documented temporary response, or a retry instruction are possible policies. Persist completed state and its result consistently with database changes where feasible. Do not treat the presence of any key row as evidence that the order has finished successfully.

Handle external effects and crash windows

A payment provider or email service does not automatically participate in the database transaction. A crash after an external effect but before storing the completed result creates a recovery problem. A durable outbox, provider-supported deduplication, or explicit reconciliation may form part of a solution, depending on the effect. Each has its own contract. Simply wrapping the local key insertion in a transaction does not establish exactly-once behavior across systems.

Document retention and recovery limits

State how long keys and results are retained, what a replay returns, and what happens after expiration. Removing a record can allow a later request with the same key to become a new submission. Also define recovery for abandoned processing states and whether failed attempts are reusable. These choices affect both correctness and storage. A client must be able to distinguish a safe retry from creating a new logical operation.

Things to check

  • Reuse a key only for the same logical operation.
  • Scope lookup to the authenticated caller.
  • Define payload equivalence and mismatch behavior.
  • Make the claim atomic and unique.
  • Plan recovery for external effects and expired keys.

This guide describes a hypothetical application contract, not a complete server implementation or a universal Idempotency-Key standard. Database uniqueness and HTTP idempotency do not alone establish exactly-once external side effects.

Sources

  1. MDN: HTTP idempotency ↗
  2. PostgreSQL: unique constraints ↗
  3. PostgreSQL: atomic INSERT ON CONFLICT ↗
Back to top ↑