Skip to content
Mohit Swami

$ note

Idempotency in Real Systems: Patterns That Actually Work

Practical approaches to duplicate requests, retries, and message replays.

reliabilityidempotencymessagingbackend

Mar 05, 2024 · 4 min read

Why idempotency fails in practice

Most failures are not clean. Requests time out after the server has already processed them. Queues redeliver messages. Clients retry because they cannot see the outcome. When your system treats those retries as new work, you create duplicate records, double charges, and inconsistent state.

Idempotency is the discipline of treating repeated inputs as the same command. In practice, teams fail to implement it because it is spread across layers. It is not just an API feature. It affects data modeling, message handling, and user experience.

Field note

Idempotency is a contract: you can repeat a request without changing the final state.

Pattern 1: Idempotency keys at the edge

For HTTP APIs, the simplest approach is a client-provided idempotency key. The server stores the result of the first request, keyed by that value. Subsequent requests with the same key return the original result.

Key management matters. Use a scoped key tied to the user and endpoint, store it with a TTL, and make sure the response is deterministic. If the response depends on “current time” or ambient state, store the response payload itself.

Pattern 2: Deterministic writes with unique constraints

You can build idempotency into the data model by creating deterministic identifiers and enforcing uniqueness at the database layer. For example, a payment reference or a composite key can prevent duplicates even if your service processes the request twice.

This pattern is strong because it does not rely on memory or cache. It does require careful schema design and clear ownership of identifiers. It also means your service must handle the “already exists” path as a success.

Pattern 3: Outbox + consumer dedupe

In event-driven systems, retries and redeliveries are normal. Use an outbox table to guarantee publishes after a transaction, then give each consumer a dedupe store keyed on message ID.

The dedupe store can be as simple as a database table with a unique constraint. Keep it bounded with TTLs and monitor its growth. Without pruning, the dedupe store becomes a silent performance problem.

Pattern 4: Stateful workflows

For long-running workflows, store state transitions explicitly. Each step should have an idempotent token so that replays advance the workflow only once. A state machine or saga framework helps, but the real requirement is that every step can be safely retried.

The workflow state should be observable. When a step fails, you should know exactly which inputs were used, which downstream systems were called, and how to replay just that step.

Idempotency and user experience

Users need consistent feedback during retries. Returning a different error each time makes the system feel unreliable even if it is technically correct. Surface the same reference ID and status for repeated actions, and make progress visible when work is async.

When operations are long-running, design APIs to be idempotent by default: create a request resource, return its ID, and allow clients to poll. Retries then become status checks rather than duplicated commands.

Retry budgets matter. If clients retry forever, the system will see endless duplicates. Use clear error codes, document expected retry windows, and return Retry-After to guide clients toward predictable behavior.

Observability and backfills

Idempotency is invisible when it works, so you need to measure it. Track duplicates avoided, retries processed, and the ratio of deduped events to successful events. That data helps you spot clients or services that are retrying too aggressively.

Backfills are the real test. If you can replay a week of events without corrupting state, your idempotency strategy is likely solid.

In regulated contexts, keep a reconciliation job that validates idempotency assumptions on a schedule and flags drift before it becomes a production incident.

A small checklist

  • Every external request has a unique, traceable identifier.
  • Database constraints prevent duplicate writes.
  • Message handlers can safely process the same event twice.
  • Retry behavior is explicit, not accidental.
  • Monitoring exposes the volume of deduped work.

Idempotency is not a checkbox. It is the basis for stable systems in the presence of failure, and it should be treated as a first-class design constraint.

$ more