Designing Idempotent Consumers

A practical lesson on idempotent consumer design, including deduplication records, safe state transitions, and how to prevent duplicate business effects under retry and replay.

Idempotent consumers turn duplicate delivery from a business risk into a manageable system condition. An idempotent consumer does not merely receive the same event twice without crashing. It handles repeated delivery in a way that does not create harmful duplicate business effect. That distinction matters. A handler that runs twice successfully and sends two invoices is reliable from one perspective and unacceptable from another.

Idempotency is therefore about effect, not only about code execution. The consumer may still be called repeatedly. The architecture goal is that the meaningful outcome is safe under retry, replay, and redelivery.

    flowchart TD
	    A["Receive event"] --> B{"Seen event or effect already applied?"}
	    B -->|Yes| C["Return safely without duplicate effect"]
	    B -->|No| D["Apply durable business change"]
	    D --> E["Record handling or effect identity"]

What to notice:

  • the consumer needs a durable basis for deciding whether work already happened
  • idempotency can be implemented through state, not just branching logic
  • the safe decision point must line up with the real business effect

Common Idempotency Strategies

Several approaches are common:

  • record processed event IDs per consumer
  • use a business idempotency key, such as paymentIntentId
  • design writes as replace-or-upsert instead of append-only duplication
  • make external API calls with provider-supported idempotency keys
  • store a state version or sequence and reject older or repeated transitions

No single technique fits every workflow. The correct method depends on whether the effect is local state, a downstream API call, a notification, or a long-lived projection.

Event ID Tracking

The simplest model is often a processed-event table keyed by consumer and event ID. If the handler sees the same ID again, it exits safely instead of repeating the side effect.

1create table processed_events (
2  consumer_name varchar(100) not null,
3  event_id varchar(100) not null,
4  processed_at timestamp not null,
5  primary key (consumer_name, event_id)
6);

This approach works well for many side-effecting consumers, but it has limits. If the event ID changes while the business intent is actually the same, pure event-ID tracking may not collapse the duplicate the business cares about.

Business-Level Idempotency

Sometimes the safer boundary is the business action itself. A payment service may use paymentIntentId or chargeRequestId as the idempotency key because the business wants “one charge for this intent,” not merely “one response to this event record.”

1async function handleChargeRequested(event: ChargeRequestedEvent) {
2  await paymentGateway.charge({
3    idempotencyKey: event.data.chargeRequestId,
4    amount: event.data.amount,
5    customerId: event.data.customerId,
6  });
7}

This example shows the stronger idea: the downstream side effect is protected using the business identity of the action, not only the transport identity of the event.

Safe State Transitions

Idempotency also appears in how state is stored. A consumer that appends a new “paid” row every time it sees payment.settled is much less safe than one that updates state to a stable terminal value. Replace-style state transitions, compare-and-set guards, and version checks often make duplicate handling simpler.

1consumerRule:
2  event: payment.settled
3  applyIf:
4    currentStatus:
5      notIn:
6        - settled
7        - refunded
8  onDuplicate: ignore

The exact syntax is not important. The point is that the state machine itself participates in idempotency.

What Idempotency Does Not Mean

Idempotency does not mean:

  • every repeated delivery is invisible to operations
  • every workflow can ignore sequence
  • every side effect can be retried forever
  • the consumer no longer needs observability or failure classification

An idempotent consumer can still fail. It is simply safer under repetition. It still needs monitoring, retry design, and clarity about which effects are protected and which are only partially so.

Common Mistakes

  • using only timestamps to detect duplicates
  • storing processed event IDs without aligning them to the real business effect
  • making the database write idempotent but forgetting the external API call
  • assuming notification duplicates are always harmless
  • treating replay the same as live delivery without checking effect safety

Design Review Question

A team tracks processed event IDs in its database but calls an external billing API without any idempotency key. They claim the consumer is idempotent because duplicate events are ignored locally. What is the strongest challenge?

The strongest challenge is that local deduplication does not fully protect the external side effect. If the consumer crashes after calling the billing API but before recording the event as processed, a retry may call the API again. The design needs a duplicate-safe boundary at the external effect itself, not only in local bookkeeping.

Quiz Time

Loading quiz…
Revised on Thursday, April 23, 2026