Idempotency from receipt to business effect

Idempotency makes repeated processing converge on the intended result. It is most useful when a sender cannot distinguish a failed operation from a completed operation whose acknowledgement was lost.

Retrying an invoice approval

Example command contract with a stored result

Stored identity

Authorised scope
org_07 / invoice-approval
Request key
req_1042
The server derives and checks this scope. A claimed organisation ID grants no authority.

Stored request

Invoice
inv_1042
Amount
125,000 GBP minor units
Local outcome
Approval recorded
The invoice, amount and authorized scope form the request fingerprint stored alongside the result.

Matching retry

If the receipt is retained and the request matches, reuse the recorded outcome without applying the update again. The reply itself may differ.

Same key with a different amount

Stored 125,000Incoming 130,000GBP minor units in both requests

Reject conflicting key reuse

The changed amount makes this a different request. Reject the key reuse under the request contract; do not return the old approval as if it applied to the new amount.

Define the effect that must happen once

Start with the business effect: creating a case, applying an invoice transition or issuing a notification. Message receipt is not the effect. A consumer can receive one message, perform the action, crash before acknowledging it and then receive the same message again.

HTTP defines idempotent methods in terms of their intended effect, not identical response bodies. Application operations performed through POST may also implement an idempotency contract, but clients must rely on an explicit server agreement. They should not assume that repeating an arbitrary POST is safe.

Choose a stable, scoped identity

For a CloudEvents consumer, source plus id identifies repeated delivery of the same event. A command often needs a caller supplied operation key, scoped to an authorized caller and operation type. Reusing one key for different requests should produce a detectable conflict rather than silently applying whichever payload arrived first.

Store a normalized request fingerprint when the contract requires payload equality. Define normalization carefully: field order in JSON is usually irrelevant, while currency, amount and destination are material. Never trust a client supplied tenant identifier without checking the caller's allowed scope.

Commit deduplication with the state change

When the business state and inbox share a transactional database, insert the event identity and apply the state change in one transaction. A uniqueness constraint resolves concurrent deliveries. Recording receipt before the action without an atomic boundary can lose work; recording it afterwards can allow repeated effects.

A unique constraint protects the operation identity. Store the processing outcome and timestamps alongside it, with a defined retention policy. Keep failed transactions retryable; a permanent received flag must not conceal work that never committed.

sql
CREATE TABLE consumer_inbox (
  consumer_name text NOT NULL,
  event_source text NOT NULL,
  event_id text NOT NULL,
  processed_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (consumer_name, event_source, event_id)
);

PostgreSQL schema for a consumer scoped event identity. Apply the inbox insert and business mutation in the same transaction.

Treat external effects as a separate boundary

A local database transaction cannot atomically commit an unrelated remote service call. Use an outbox to record intended work with local state, then deliver it through a worker. The outbox closes one failure gap, but the receiver still needs duplicate handling when delivery is retried.

If the external provider accepts an idempotency key, preserve it across attempts and respect its documented retention window. Otherwise use reconciliation, a queryable operation reference or human review for ambiguous results.

Confirming work in another service

Local database

Approval state and an outbox message commit together.

Message retained

Remote receiver

A worker may send the message more than once. The receiver needs its own deduplication or reconciliation contract.

Confirmation required

An inbox or outbox record alone cannot confirm that another service acted exactly once.

Align retention with the replay horizon

Deleting deduplication records makes an old delivery appear new. Set retention from the longest permitted retry, replay and recovery horizon, then decide how older events are handled. For irreversible effects, an enduring business uniqueness constraint may be more appropriate than a short cache entry.

Test simultaneous duplicates, timeouts after commit, changed payloads under one key and replay after retention expires. Verify the resulting state and count external actions to check that retries did not repeat the business effect.

References