All event-sourcing patterns
Production pattern416 words · verified 10 September 2026

Idempotent Event Consumers and Handlers

An idempotent event consumer produces the same durable result when it receives an event more than once. Use stable event IDs, entity versions, unique effect keys, and acknowledge only after processing succeeds; at-least-once delivery then becomes recoverable instead of corrupting state.

Problem

Why this pattern exists

Networks fail between committing work and acknowledging delivery. Consumer may finish update, lose connection, and receive same event again after restart. Trying to guarantee exactly-once transport across independent systems usually moves ambiguity rather than removes it. Idempotent business effects provide practical guarantee.

Different outputs require different guards. A projection can ignore an entity event whose version is not newer than last applied. A payment or email needs effect ledger keyed by stable event or command ID. An additive metric may need set membership or deterministic upsert instead of increment-on-delivery.

Design decisions

Make boundaries explicit

  1. 01

    Choose deduplication key

    Use immutable event ID for one effect per fact, command ID for one effect per request, or entity version for ordered projection state. Do not derive key from mutable payload fields.

  2. 02

    Commit before acknowledge

    Persist output and dedup marker in same transaction where target supports it. Then acknowledge event-store cursor. Crash before ack causes safe repeat; crash after ack leaves committed output.

  3. 03

    Handle gaps explicitly

    Per-entity version jump indicates missing or reordered input. Pause that entity, recover gap, then continue rather than accepting silently inconsistent projection state.

AllSource implementation

Apply pattern to durable Core history

AllSource durable consumers deliver committed events at least once. Core tracks acknowledged WAL position and replays from stored cursor after reconnect. ProjectionWorker adds per-entity version dedup as safety net, but cross-entity invariants and external effects still require application-level idempotency.

Give each deployed consumer version stable unique name, keep event-type filters narrow, and store processed event ID with outbound result. On reducer error, stop or dead-letter with enough event metadata to diagnose; do not advance cursor past an unhandled fact. Monitor repeat rate, version gaps, processing latency, reconnects, and checkpoint lag.

Idempotent effect transaction
begin transaction
  if processed_events contains event.id: return success
  upsert invoice_status from event payload
  insert processed_events(event.id, consumer = "billing_v2")
commit transaction
ack durable consumer position

Failure modes

Detect weak implementations early

Counter increments twice after consumer reconnect.

Fix: Use event-ID ledger or deterministic aggregate recomputation, not blind increment.

Cursor advances although target write failed.

Fix: Acknowledge only after durable target commit.

Two consumer versions share identity and move one cursor.

Fix: Version consumer IDs and run one owner per identity.

Production checklist

Ready when each statement is true

  • Every effect has stable deduplication key.
  • Output and processed marker commit atomically where possible.
  • Cursor acknowledgement follows durable processing.
  • Per-entity gaps and duplicates have explicit policy.
  • Repeat delivery and checkpoint lag are measured.

Related patterns

Store history once

Rebuild every useful view from durable events.