⏱ 20 min read
Distributed systems have a cruel sense of humor. They fail in the one place the business can least tolerate ambiguity: right after you’ve told a customer “done.”
A payment is submitted. The client times out. The user presses refresh. A mobile app retries because the train just went through a tunnel. Kafka redelivers after a rebalance. An orchestrator restarts a pod midway through handling a command. Somewhere, far from the neat arrows of the architecture slide, the same business intent arrives twice. Or three times. Or ten. event-driven architecture patterns
And this is where many microservice programs reveal what they actually believe. microservices architecture diagrams
They say they care about correctness, but their services still treat retries as plumbing and duplication as an edge case. They say they are event-driven, but they have no disciplined story for replay. They say they are “cloud native,” but they still quietly depend on a human operator cleaning up duplicates in the back office on Monday morning.
That’s not architecture. That’s wishful thinking with YAML.
Idempotency should be a first-class concern in microservices because retries are not rare. Retries are the atmosphere. In any serious enterprise estate—HTTP APIs, Kafka consumers, sagas, batch integrations, partner systems, mobile clients, workflow engines—duplicate delivery is not an exception path. It is a normal operating condition. If the business process cannot absorb repetition without corruption, the architecture is brittle by construction.
The right way to think about idempotency is not as a technical ornament on endpoints. It is a domain decision about intent, identity, and outcome. It belongs beside concepts like consistency boundaries, invariants, and business capabilities. In domain-driven design terms, idempotency lives where commands meet aggregates, where policies meet state transitions, and where ubiquitous language distinguishes “the same request repeated” from “a new instruction that happens to look similar.”
That distinction matters more than most teams admit.
A duplicate “Ship Order” command should not create two shipments. A duplicate “Reserve Credit” command should not double-hold funds. But two “Add Item” commands with the same SKU may be either duplicates or legitimate repeats depending on the domain semantics. If your architecture does not encode that difference explicitly, you are not building reliable services. You are just hoping your infrastructure behaves politely.
This article takes a hard line: idempotency is not optional in microservices that handle money, inventory, customer commitments, regulatory workflows, or any process with replay, retries, or asynchronous messaging. We’ll look at the forces that make it essential, the architecture patterns that make it work, the migration path from legacy estates, the operational realities, and the failure modes that catch teams who reduce the subject to “just store a request ID somewhere.”
Context
Microservices changed the failure profile of enterprise systems.
In a monolith with a single ACID database, duplicate handling often hid inside transaction isolation and unique constraints. The world was still messy, but the mess was centralized. In microservices, the same business workflow is split across network calls, local transactions, asynchronous events, caches, brokers, and external systems. Every boundary introduces uncertainty. Every retry risks repetition. Every consumer restart reopens the question: “Have I already done this?”
This is especially visible in event-driven architectures using Kafka. Kafka gives you durable logs, replayability, partition ordering, and high-throughput messaging. It does not give you magical business exactly-once semantics end to end. It can help with producer and stream-processing guarantees under specific conditions, but if your service writes to a database, calls a payment gateway, publishes another event, and updates a read model, then “exactly once” evaporates into a chain of local guarantees and compensating assumptions.
The enterprise consequence is simple: systems must be designed to tolerate duplicate delivery and repeated execution while preserving business correctness.
That means idempotency is not merely an API feature. It spans:
- synchronous commands over HTTP or gRPC
- asynchronous commands and integration events over Kafka
- orchestration and saga steps
- scheduled jobs and reprocessing pipelines
- reconciliation processes
- operator-triggered manual retries
- recovery after partial failure
If a service can be asked twice, it needs a coherent answer.
Problem
The common implementation mistake is to think of idempotency as request deduplication. That is too narrow.
The real problem is preserving domain truth when the same business intent is observed more than once across unreliable delivery paths.
Consider a retail order flow:
- Customer places an order.
- Order service creates the order.
- Payment service authorizes the card.
- Inventory service reserves stock.
- Fulfillment service creates a shipment.
- Events are published to Kafka for downstream consumers.
Any of these steps may be retried because of timeout, crash, redelivery, or uncertainty. If idempotency is absent or inconsistent, the system can drift into pathological states:
- duplicate charges
- multiple inventory reservations
- duplicate shipments
- repeated loyalty point awards
- inconsistent read models
- conflicting downstream notifications
- financial mismatches requiring reconciliation
Worse, duplicate effects are not always obvious immediately. Many enterprises discover them later through customer complaints, warehouse confusion, ledger imbalances, or audit findings. The damage is then both operational and reputational.
The heart of the problem is that distributed systems frequently cannot tell the difference between “did not happen” and “happened, but I didn’t see the acknowledgment.” That uncertainty drives retries. Retries drive duplicates. Therefore architecture must encode sameness.
Forces
A good architectural decision survives contact with competing forces. Idempotency sits in the middle of several.
At-least-once delivery is normal
Kafka consumers may reprocess messages after rebalance, offset commit timing, or failure. HTTP clients retry after timeout. Workflow engines retry failed tasks. Humans click twice. Networks lie. The architecture must assume repeated delivery.
Domain semantics vary
Not every repeated request is a duplicate. “Transfer $100 from A to B” repeated with the same command ID is likely a duplicate. “Increase credit limit by $100” repeated may be catastrophic. “Set customer address to X” is naturally idempotent. “Append note” is not. You cannot solve this at the transport layer alone because the meaning lives in the domain.
Availability pushes retries
Modern systems optimize for resilience through retry policies, exponential backoff, circuit breakers, and queue-based buffering. These practices improve availability but increase duplicate pressure. Reliability mechanisms amplify the need for idempotency.
Consistency boundaries are local
Each microservice owns its own data. Cross-service transactions are avoided. That means duplicate protection must often happen within local aggregate boundaries and through integration contracts, not through a global transaction manager.
Storage and retention are not free
Persisting idempotency keys, responses, or processed event markers costs storage, index maintenance, and operational care. Retention windows require business and legal decisions. There is no free lunch.
External systems may not cooperate
A payment gateway may support an idempotency key. An ERP might not. A shipping partner may treat duplicate requests as distinct orders. Internal correctness often depends on integrating with systems that have weaker semantics than your own design.
Reconciliation is inevitable
Even with good patterns, some effects remain uncertain or externally inconsistent. Enterprises need reconciliation jobs, audit trails, and compensating workflows. Idempotency reduces the blast radius; it does not eliminate operational reality.
Solution
Treat idempotency as a domain capability, not a middleware trick.
That means three practical shifts.
First, model business intent explicitly. Commands need stable business identity. “CreatePayment” should carry a payment instruction identity. “ReserveInventory” should carry a reservation identity scoped to an order line or fulfillment request. An idempotency key should not be random decoration detached from the domain. It should represent “this exact intent, once.”
Second, make state transitions safe to repeat. A repeated command should either:
- return the original result,
- produce no additional effect,
- or be rejected as semantically conflicting.
Third, connect local idempotency to message-driven workflows. A service must be able to consume duplicate messages, avoid duplicate side effects, and publish downstream events without creating gaps or double emission. This is where patterns like the transactional outbox, inbox table, deduplication store, and reconciliation process become part of one coherent design.
The essential architecture is straightforward but often implemented poorly:
- clients send a command with an idempotency key or domain command ID
- the receiving service persists the command identity within its local transaction boundary
- aggregate logic checks whether this intent has already been applied
- if not applied, state changes occur and an outbox event is recorded
- if already applied, the previous result is returned or the operation is treated as no-op
- downstream consumers record processed message IDs before or with local state changes
- reconciliation detects and repairs gaps where partial failure escaped local guarantees
That is the broad shape. The devil, as always, lives in the semantics.
Architecture
The simplest case is an HTTP command to a service that owns the aggregate.
This is not glamorous. It is deliberately boring. Boring is good. Enterprise architecture is full of systems that failed because teams chased elegant messaging myths instead of preserving business truth.
There are a few key decisions embedded here.
Idempotency key design
The idempotency key must align to business intent and scope. A naive globally unique token generated per network attempt is useless. You need a key that remains stable across retries of the same intent.
Good examples:
paymentInstructionIdorderSubmissionIdreservationIdpartnerRequestIdworkflowTaskId
Sometimes the client can generate the key, especially in public APIs and mobile applications. Sometimes the platform or upstream workflow assigns it. Sometimes the domain already has a natural identifier.
The key also needs scoping. A lineItemId may be unique only within an order. A partnerReference may be unique per partner. The architecture must encode the uniqueness boundary. This is classic domain-driven design: identity is contextual.
Aggregate-level semantics
The aggregate is where idempotency becomes meaningful. A repeated command must be classified based on domain rules.
For example:
SetDeliveryAddress(orderId, address)is idempotent if the address is the same and the order remains modifiable.AddOrderLine(orderId, sku, qty)is not automatically idempotent because two requests may intend to add quantity twice.CapturePayment(paymentId, amount)may be idempotent only if the exact amount matches the prior authorized/captured state.CreateCustomer(customerNumber)is idempotent if customerNumber is a true business identity; it is not if the command means “register another profile.”
The transport can deduplicate bytes. Only the domain can deduplicate meaning.
Consumer-side idempotency
On the Kafka side, the receiving service must assume duplicate events or commands.
A common pattern is an inbox or processed-message table. The consumer writes the message ID together with its local state changes in the same transaction. If the message is seen again, it is ignored or handled as replay.
This is one of those patterns that sounds obvious after you’ve lived through not having it.
Without it, redelivery after consumer crash can double-apply effects. With it, replay becomes routine. Kafka retention becomes your ally instead of your enemy.
The outbox-inbox pair
The transactional outbox solves one half of the reliability problem: you commit business state and the intent to publish as part of one local transaction. A relay then publishes the outbox entries to Kafka.
The inbox or processed-message table solves the other half: consumers apply a message once from the perspective of local side effects.
Together, they create a practical end-to-end idempotent architecture across microservices without pretending a distributed transaction exists.
Retry loops are part of the design
Retries should be intentional, bounded, and visible.
That last branch matters. Not every repeat should quietly succeed. If the same idempotency key arrives with a materially different payload, the service should detect semantic conflict and reject it. Otherwise you risk treating contradictory commands as duplicates.
Reconciliation closes the gaps
Even a careful outbox/inbox design does not erase all uncertainty:
- external partner accepted a request but your system timed out
- payment gateway charged but response was lost
- outbox relay lagged during incident
- a consumer bug processed data incorrectly before dedup markers were fixed
- an old legacy system emitted duplicate records without stable IDs
This is where reconciliation enters. Reconciliation is not an admission of defeat; it is a standard enterprise control. Periodically compare authoritative sources, identify divergence, and repair.
A mature architecture plans for:
- replayable event logs
- auditable command histories
- reconciliation jobs by business capability
- manual exception queues
- compensating actions with domain approval
Migration Strategy
Most enterprises cannot stop everything and redesign idempotency from scratch. They have legacy services, brittle integrations, shared databases, and workflows that grew by sediment rather than intent. So the migration strategy matters as much as the target architecture.
The right move is progressive strangler migration.
Start at the edges where retries and customer-visible duplication hurt most. Wrap legacy operations behind a new command API that requires a stable request identity. Introduce an idempotency store in the façade if the core system cannot yet support duplicate-safe semantics internally. Then gradually push the logic inward toward proper aggregate boundaries.
A practical sequence looks like this:
- Inventory duplicate hotspots
Find where duplicate effects already happen: payments, orders, shipment creation, customer registration, claims, policy issuance, invoicing.
- Define domain intent identities
Work with domain experts to name commands and their stable identifiers. This is a DDD exercise, not just an API design exercise. API architecture lessons
- Add idempotent façade endpoints
Introduce APIs that accept command IDs or idempotency keys and return stable outcomes for retries.
- Implement local deduplication store
Persist command identity, status, and prior response. Even if ugly at first, this creates a control point.
- Adopt transactional outbox
Where services publish Kafka events, ensure state change and event intent are committed atomically.
- Add consumer inbox/processed-message tracking
Critical consumers must be replay-safe before you lean heavily into event-driven integration.
- Introduce reconciliation workflows
Especially for external systems and legacy cores, where exact duplicate prevention may remain imperfect.
- Strangle legacy direct calls
Move callers to the new semantic command interfaces, then retire ad hoc retries and implicit duplicate handling.
This is not glamorous transformation work. It is the sort of migration that saves millions quietly.
One caution: a façade-only idempotency layer can become a trap if left too long. It may suppress duplicate HTTP requests while the underlying domain still emits duplicate business effects through batch jobs, direct database integrations, or partner replays. Use the façade as a bridge, not a permanent substitute for proper domain behavior.
Enterprise Example
Take a large insurer handling claims across web channels, call centers, document ingestion, fraud checks, payment disbursement, and a policy administration platform from another geological era.
A claimant submits a claim online. The front-end occasionally retries because photo uploads are slow and mobile connectivity is unstable. The claims service emits ClaimSubmitted to Kafka. Downstream services start fraud screening, reserve estimation, document requests, and payment eligibility workflows. Some services are modern. Others are wrappers around core platforms and vendor systems.
Before idempotency was treated as architecture, the insurer saw familiar symptoms:
- duplicate claims created from repeated submissions
- duplicate document request emails
- repeated fraud screening charges from a third-party vendor
- duplicate payment instruction attempts after workflow retries
- manual back-office effort to merge and correct records
The fix was not “turn on exactly-once.” There is no such switch for reality.
They introduced a claimSubmissionId generated at the channel and preserved across retries. The claims service treated SubmitClaim as an idempotent command scoped by claimant and incident context. If the same submission arrived again, it returned the existing claim reference and status.
Kafka consumers handling ClaimSubmitted adopted inbox processing keyed by event ID and claim ID. Payment instruction creation used a separate disbursementInstructionId, because payment is a different business intent from claim submission. That distinction matters. One claim may legitimately lead to multiple payments over time. Reusing the claim ID as the payment idempotency key would have been wrong.
The insurer also had to build reconciliation with the payment provider because acknowledgments occasionally timed out. Daily comparison of internal disbursement instructions against provider settlement files identified uncertain outcomes. Where duplicates or mismatches appeared, compensating workflows and operator review resolved them.
The result was not theoretical purity. It was fewer duplicate payouts, cleaner audit trails, lower support cost, and a claims process the business could trust under retry and replay.
That is what good architecture looks like in enterprises: less drama, more control.
Operational Considerations
Idempotency is not done when the code compiles. It changes operations.
Retention windows
How long do you retain idempotency keys and processed message records? Hours may be enough for client retries. Payments or partner integrations may need weeks or months. Retention is a business risk decision wrapped in a storage decision.
Observability
Track:
- duplicate request rate
- replay rate
- inbox dedup hits
- outbox lag
- reconciliation mismatch counts
- semantic conflict rejects
- DLQ volume and age
If you cannot see your duplicate-handling behavior, you cannot govern it.
Semantic conflict detection
If the same idempotency key arrives with a different payload, log and reject clearly. This usually indicates a client bug, key misuse, or a workflow design flaw.
Partitioning and scaling
Deduplication stores need indexing and partition strategy. Hot keys can appear in highly retried workflows. Kafka partitioning by aggregate identity often helps preserve order where the domain needs it, but it must be matched to consumer concurrency and throughput expectations.
Replay readiness
A service that claims to be event-driven should be able to replay topics safely. That means idempotent consumers, version-tolerant event handling, and enough auditability to explain outcomes after reprocessing.
Data protection and compliance
If idempotency records include payload snapshots or prior responses, be careful with personal data, financial data, and retention obligations. The simplest implementation often violates a policy someone important will eventually notice.
Tradeoffs
Idempotency is worth doing, but it is not free.
The first tradeoff is complexity. You are adding stores, keys, conflict logic, response caching, outbox relays, inbox tables, and reconciliation processes. Teams that underestimate this usually end up with partial solutions that create false confidence.
The second is storage and write amplification. Every command may now write extra records. Every consumed event may touch a dedup table. At enterprise scale, this matters.
The third is latency. A service may need to check prior command state before executing. Usually acceptable, sometimes painful.
The fourth is semantic design effort. Domain experts and architects must decide what counts as “the same intent.” This is not clerical work. It can expose unresolved business ambiguity.
The fifth is operational burden. Reconciliation and exception handling need ownership. Without it, idempotency covers easy cases while hard inconsistencies rot in side channels.
Still, the alternative is worse. Systems without deliberate idempotency often push the cost into finance teams, customer support, warehouse staff, and auditors. That is not simplification. It is architectural debt disguised as organizational heroics.
Failure Modes
There are several classic ways teams get this wrong.
Random key per retry attempt
If the client generates a new idempotency key on every retry, the server cannot correlate duplicates. This is surprisingly common and almost useless.
Key detached from domain semantics
A generic transport ID may deduplicate network attempts but fail to represent business intent. Then duplicate effects still occur through other paths.
No payload consistency check
If the same key is reused for different payloads and the service silently returns the first result, you create hidden data corruption and painful support incidents.
Dedup record written outside local transaction
If you record “processed” before the business state commits, a crash can cause message loss. If you record it after, a crash can cause duplicate side effects. Transaction boundaries matter.
Assuming Kafka exactly-once solves business semantics
It does not. It helps within specific producer/consumer pipelines, but external side effects and databases remain your responsibility.
Ignoring external systems
Your internal services may be perfectly idempotent while the downstream payment processor or ERP creates duplicates on every timeout retry. End-to-end architecture must include partner semantics.
No reconciliation
Some uncertain outcomes cannot be resolved in-band. Without reconciliation, incidents become archaeology.
When Not To Use
Here’s the unfashionable part: not every operation needs heavy idempotency machinery.
Do not over-engineer it for:
- ephemeral telemetry
- low-value notifications where duplicates are harmless
- analytics pipelines tolerant of eventual aggregate correction
- strictly internal computations without side effects
- one-off administrative scripts that are already manually controlled and low risk
Also, if the domain operation is inherently non-repeatable and retries should never happen automatically, then the better architecture may be to disallow automatic retry and require explicit operator resolution. A good example is some high-risk trading or irreversible legal filing workflows. Even there, you still need duplicate detection—but perhaps not a blanket “just replay safely” model.
The point is not to spray idempotency keys everywhere like holy water. The point is to apply it where repeated intent collides with consequential side effects.
Related Patterns
Idempotency sits in a family of patterns that work together.
Transactional Outbox
Commit business state and outgoing event intent together, then publish asynchronously.
Inbox / Processed Message Table
Ensure consumers can handle redelivery safely.
Saga / Process Manager
Coordinate multi-step workflows where each local action may need idempotent execution and compensating behavior.
Reconciliation
Detect and repair gaps that escape transactional boundaries, especially with external systems.
Optimistic Concurrency Control
Helps prevent conflicting updates, though it is not the same as idempotency.
Natural Keys and Business Identifiers
Critical for modeling intent identity in domain-driven design.
Strangler Fig Pattern
Useful for migrating legacy systems gradually toward explicit command semantics and duplicate-safe behavior.
These patterns are not a menu of independent options. In real enterprise architecture, they reinforce one another.
Summary
Idempotency is one of those topics that looks technical until you watch a business pay for getting it wrong.
In microservices, retries are routine, Kafka replay is a feature, and partial failure is normal. The same business intent will arrive more than once. If the architecture cannot distinguish repetition from novelty, it will eventually duplicate money, stock, messages, or customer commitments. And then the organization will compensate with manual work, reconciliation panic, and brittle exception handling.
The better approach is to make idempotency a first-class concern.
Model intent identity in the domain. Align keys with business semantics. Make aggregates safe for repeated commands. Use transactional outbox and consumer inbox patterns. Design retry loops intentionally. Add reconciliation because the world outside your service boundary is imperfect. Migrate progressively through strangler techniques rather than pretending a greenfield rewrite is coming to save you.
Most of all, be honest about tradeoffs. Idempotency adds complexity. But so does every duplicate payout, duplicate shipment, and duplicate customer promise.
Reliable systems do not avoid retries. They survive them.
That is the real architectural standard.
Frequently Asked Questions
What is a service mesh?
A service mesh is an infrastructure layer managing service-to-service communication. It provides mutual TLS, load balancing, circuit breaking, retries, and observability without each service implementing these capabilities. Istio and Linkerd are common implementations.
How do you document microservices architecture for governance?
Use ArchiMate Application Cooperation diagrams for the service landscape, UML Component diagrams for internal structure, UML Sequence diagrams for key flows, and UML Deployment diagrams for Kubernetes topology. All views can coexist in Sparx EA with full traceability.
What is the difference between choreography and orchestration in microservices?
Choreography has services react to events independently — no central coordinator. Orchestration uses a central workflow engine that calls services in sequence. Choreography scales better but is harder to debug; orchestration is easier to reason about but creates a central coupling point.