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.
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)
);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.
Local database
Approval state and an outbox message commit together.
Message retainedRemote receiver
A worker may send the message more than once. The receiver needs its own deduplication or reconciliation contract.
Confirmation requiredAn 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.