Idempotency as a System Boundary, Not a Header
Idempotency is often implemented as an HTTP convenience. In production systems, it is a boundary decision that determines how duplicates, retries, and partial failures are contained.
Idempotency gets described too narrowly. Many teams think about it only when they add a payment endpoint or a public API method that might be retried by a client. The implementation then becomes mechanical: accept an idempotency key, store a response, replay it if the same key appears again. That is useful, but it is not the whole problem.
In real systems, duplicates do not originate from a single header. They emerge from network retries, queue redelivery, user refreshes, load balancer timeouts, webhook replay, and operators running the same backfill twice because the first run produced ambiguous results. If your only idempotency story lives at the HTTP edge, the system is still duplicate-prone in most of the places that matter.
The better framing is this: idempotency is a property of a boundary. A boundary is any place where an operation can be observed, retried, replayed, or partially completed. Payments have one. Job execution has one. Inventory reservation has one. Email dispatch has one. Once you frame the problem this way, the implementation choices become more coherent.
The failure mode of header-only thinking#
Header-only idempotency assumes the operation can be fully defined and contained at the request layer. That assumption fails quickly in distributed systems. A request may succeed in writing a database record but fail before returning a response. It may enqueue downstream work twice. It may publish one event and fail before publishing another. A retried request now lands on a system that is no longer in its pre-request state.
The result is a dangerous illusion. The API appears idempotent because it accepts the same key twice, but the rest of the workflow is not. Duplicate shipments, double notifications, and repeated side effects still happen downstream. Teams are then surprised because they "already implemented idempotency."
What they actually implemented was request deduplication at one ingress point.
A better model, the boundary owns deduplication#
The right question is not "Do we have an idempotency header?" The right question is "Where do we define the identity of the operation, and where do we enforce that it can complete at most once?" That is a boundary decision.
For a payment capture, the boundary may be the combination of merchant, order, and capture intent. For a webhook consumer, it may be the provider event ID plus the consuming handler. For a background job, it may be a domain key like invoice ID plus job type. Once that identity is explicit, you can persist it near the state transition that matters.
That typically means one of three mechanisms. You can use a uniqueness constraint in the primary database. You can maintain a durable deduplication table keyed by the operation identity. Or you can model the operation as a state machine where duplicate transitions become no-ops. All three are stronger than trusting the client request alone.
The boundary must include side effects#
This is where many systems still fail. Engineers deduplicate the write that creates a row, but not the side effects that follow. The result is a single order record paired with duplicate emails, duplicate queue messages, or duplicate external calls.
If the boundary includes side effects, then the system needs a durable record of whether those effects were already scheduled or executed. An outbox pattern is often the cleanest move. You commit the state transition and the outbound intent in the same transaction, then downstream workers process the outbox idempotently. Now retries at the HTTP layer and retries in the async layer both have somewhere consistent to land.
Members continue here
This article continues for ArchCrux members.
Create your account to keep reading, then unlock the full archive when you are ready.
Or read a free article firstOr subscribe to our free newsletter.
Get notified when new articles publish.
Retries become safe only when ambiguity is modeled#
Production retries are about ambiguity. Did the previous attempt fail before the state change, after the state change, or during side-effect fanout? If the system cannot answer that, every retry is a gamble. Idempotency exists to turn that gamble into a deterministic lookup.
This is why the deduplication store must live close to the authoritative state transition. If it is in a volatile cache, or in a separate store with weaker durability than the business write, you can still lose the record that tells you whether the operation already happened.
Operational consequences#
Once you treat idempotency as a boundary, you get better dashboards and better incidents. Instead of counting duplicate headers, you can count duplicate operation identities. You can measure how often retries hit an already-completed state. You can observe where ambiguity accumulates, such as webhook handlers that repeatedly time out after the database write but before acknowledging the upstream provider.
That turns idempotency from a checklist item into a reliability tool. It also makes architectural reviews sharper. Teams stop debating whether a mobile client should generate UUIDs and start debating where the system can safely absorb ambiguity.
The pattern is simple to state. Define the identity of the operation. Persist it durably at the boundary that owns the state transition. Make duplicate attempts resolve against that durable record instead of replaying side effects blindly. When that is true, retries stop being scary. They become part of the normal operating model.