Side Effects in Event Handlers in Event-Driven Systems

⏱ 18 min read

Event-driven systems have a habit of looking cleaner on whiteboards than they do in production. On a slide, an event leaves one service, drifts through a broker, and lands in another service with all the elegance of a relay baton. In the real world, that baton is often on fire.

The trouble is not events themselves. Events are usually the honest part of the system. They say, plainly, that something happened: an order was placed, a payment was captured, a shipment was delayed. The trouble begins in the handler. That is where side effects gather—database writes, HTTP calls, emails, ledger updates, cache invalidations, fraud checks, inventory reservations, and a dozen “small” integrations that nobody thought would matter. The handler becomes a pressure point where domain meaning, operational risk, and accidental coupling collide.

This is where many event-driven architectures quietly go wrong. Teams celebrate asynchronous decoupling while wiring business-critical side effects into opaque chains of handlers. They get scalability and flexibility, yes. They also get retries that duplicate actions, ordering anomalies that break invariants, and event chains so long that a customer refund can end up depending on a marketing webhook. The architecture starts to behave less like a well-designed business system and more like a row of mousetraps connected with string.

The answer is not to avoid side effects. That would be absurd. Software exists to cause effects. The answer is to become deliberate about where side effects live, what domain semantics they represent, how they fail, and how they can be reconciled when the world refuses to behave transactionally. In enterprise systems—especially those built on Kafka, microservices, and long-lived business workflows—this is not a coding detail. It is architecture. event-driven architecture patterns

This article is about that architecture: how to think about side effects in event handlers, how to keep domain semantics intact, how to migrate from fragile event chains, and when not to use this style at all.

Context

In an event-driven system, one component emits events and other components react. That simple model scales remarkably well because it lets producers publish facts without waiting for all consumers to finish their work. Kafka, Pulsar, cloud event buses, and message brokers make this pattern cheap enough that enterprises now use it for everything from payment processing to manufacturing telemetry.

But there is a subtle distinction that matters enormously: an event is a record of something that happened in the domain, while a handler is a decision to do something next.

That sounds obvious. It is not treated as obvious.

Teams often blur the line between domain events and integration behavior. A service emits OrderPlaced, a handler charges the card, another handler reserves inventory, another creates loyalty points, another updates a reporting store, and yet another sends a discount campaign. Suddenly one domain fact has become a pinball machine of side effects. The original event was stable. The chain built on top of it is not.

Domain-driven design is useful here because it reminds us that software structure should reflect business language, not infrastructure convenience. An OrderPlaced event means something in the Ordering bounded context. A ReserveInventory action belongs to Inventory semantics. A CapturePayment action belongs to Payments semantics. Those are not interchangeable. A domain event should express what has happened. A command or process decision should express what ought to happen next.

If you ignore that distinction, your architecture becomes semantically muddy. Muddy semantics lead to muddy ownership. Muddy ownership leads to handlers with too much authority. And those handlers become the place where enterprise systems leak.

Problem

The core problem is simple: event handlers often contain side effects that are both operationally dangerous and semantically ambiguous.

A handler may:

  • update local state
  • call another microservice
  • produce additional events
  • invoke third-party APIs
  • trigger customer communications
  • coordinate a long-running workflow

Each of those effects introduces its own failure model. Combined inside a single handler, they create a system that is easy to start and hard to reason about.

Consider a common Kafka-based flow:

  1. Ordering publishes OrderPlaced.
  2. Payment handler receives it and calls a payment gateway.
  3. Payment handler writes PaymentCaptured.
  4. Inventory handler consumes PaymentCaptured and reserves stock.
  5. Shipping handler consumes InventoryReserved and creates fulfillment.
  6. Notification handler emails the customer.

On paper, this looks decoupled. In practice, it is a distributed transaction disguised as a playlist.

If the payment gateway times out but actually captures funds, the retry may charge the customer twice unless idempotency is enforced. If inventory reservation fails after payment succeeds, you need compensation or manual intervention. If email fails, should the business workflow fail? Probably not. If the event schema changes, who owns compatibility? If handlers process out of order, what becomes of your business invariant?

The deeper issue is that side effects are rarely equal. Some are domain-critical. Some are integration-critical. Some are merely informative. Treating them all as just “consumers of events” is a category error.

Forces

A good architecture exists because it balances forces, not because it follows a pattern diagram.

Here, the forces are unusually strong.

1. Business workflows span bounded contexts

Ordering, Billing, Inventory, Shipping, Risk, and CRM each have their own models and rules. DDD tells us these contexts should not collapse into one another. But the business process still crosses them. Something has to coordinate without destroying autonomy.

2. Side effects are not transactionally atomic across services

Once you cross service or broker boundaries, ACID transactions are gone or too expensive to rely on. You are living in the land of eventual consistency whether you admit it or not.

3. Brokers encourage fan-out

Kafka makes it wonderfully easy to add another consumer. That is both power and temptation. The result is often event sprawl: too many consumers hanging behavior off a domain event with no clear policy about what is core versus incidental.

4. Retries are necessary and dangerous

In distributed systems, retries are table stakes. But retries turn side effects into duplicate side effects unless handlers are idempotent and external APIs support safe deduplication.

5. Ordering is partial, not universal

Kafka gives strong ordering within a partition, not across the whole world. If your domain semantics depend on total global order, your design is already lying to you.

6. Enterprises need auditability

Regulated environments need traceability: who changed what, when, based on which business event, and what happened when downstream actions failed. Opaque chains of ad hoc handlers make compliance painful.

7. Teams want local autonomy

Microservices are usually justified by team boundaries as much as technical boundaries. Any solution that centralizes all logic in one orchestration layer may solve correctness while killing team independence.

Architecture here is a negotiation between these forces. There is no pattern that makes them disappear.

Solution

My blunt view is this: keep event handlers small, explicit, and semantically honest. Use handlers to translate facts into local state changes, durable work records, and carefully bounded process decisions. Do not let them become mystery boxes of cascading side effects.

There are three practical rules.

Rule 1: Separate domain reaction from integration side effects

A handler should first decide whether the event matters in its own bounded context. If it does, persist a local state change or work item. Then perform side effects through controlled mechanisms: outbox publication, command dispatch, workflow state transitions, or scheduled reconciliation.

This preserves domain semantics. The service is not merely reacting to broker traffic; it is making a business decision in its own model.

Rule 2: Distinguish primary from secondary side effects

Primary side effects are essential to the business process: charging a card, reserving inventory, booking a shipment. Secondary side effects are supportive: notifications, analytics, search indexing, cache refreshes.

Primary side effects deserve explicit workflow modeling, idempotency keys, state machines, and reconciliation plans. Secondary side effects should usually be isolated so their failure does not poison the primary flow.

This one distinction saves a remarkable amount of pain.

Rule 3: Design for reconciliation, not just happy-path propagation

In event-driven systems, there will be cases where the event says one thing and the world did another. A third-party API accepted a request but your timeout fired. A consumer lagged for six hours. A handler ran twice. A schema changed unexpectedly. You need periodic or triggered reconciliation to compare intended state, observed state, and emitted events.

If your architecture has no reconciliation strategy, it is not robust; it is merely optimistic.

Architecture

The most reliable architecture for side effects in handlers is a combination of domain events, local transaction boundaries, an outbox or transactional message publication pattern, and explicit process management where the business workflow requires coordination.

Here is the basic shape.

Architecture
Architecture

Notice what is absent: the handler does not consume an event and immediately spray half a dozen remote calls while hoping retries sort it out. Instead, it records local intent and publishes durable outcomes.

That still leaves an important question: where does workflow coordination live?

There are two broad models.

Choreography

Each service reacts to events and emits new events. This works well for simple flows with a small number of bounded contexts and weak coupling between steps.

It breaks down when:

  • there are many conditional branches
  • compensation logic becomes complex
  • business stakeholders need visibility into workflow state
  • failures require timed retries or escalation

Orchestration or process management

A saga orchestrator, process manager, or workflow engine tracks the state of a business process and issues commands or reacts to outcomes. This is often the better choice for primary side effects with serious business consequences.

The trick is not to over-centralize. The orchestrator should coordinate process state, not absorb all domain logic from participating services.

Orchestration or process management
Orchestration or process management

This model makes the process visible. Visibility matters. In large enterprises, “what state is this order in?” is not a trivial operational question. It is often the difference between customer service solving a problem in one minute versus opening a cross-team incident.

Domain semantics matter

A handler should not blindly infer business meaning from technical events. For example:

  • PaymentAuthorized is not the same as FundsSettled
  • InventoryReserved is not the same as InventoryAllocated
  • ShipmentCreated is not the same as ShipmentDispatched

These distinctions are not pedantry. They are the architecture. They determine what downstream side effects are valid. If your event names are vague, your handlers will become vague too, and vague handlers create bad side effects.

This is where DDD earns its keep. Bounded contexts define the language. Ubiquitous language defines the events. The events constrain what handlers are allowed to mean.

The event chain diagram is not the system

Architects love event chain diagrams because they compress complexity into arrows. Useful, yes. Dangerous too.

A proper event chain diagram must show:

  • which transitions are domain facts
  • which are commands or process decisions
  • where local durability occurs
  • where retries happen
  • where reconciliation or dead-letter handling exists

Otherwise it is just a wish.

The event chain diagram is not the system
The event chain diagram is not the system

That diagram reflects a healthier architecture because it distinguishes domain events from process actions.

Migration Strategy

Most enterprises do not get to design this from scratch. They inherit a mess: synchronous chains, ad hoc consumers, fragile retries, and event handlers with enough side effects to qualify as a workflow engine written accidentally.

So the migration strategy matters.

The right migration is usually a progressive strangler, not a rewrite.

Step 1: Find the dangerous handlers

Do not start with all event consumers. Start with the ones that combine:

  • remote calls
  • multiple side effects
  • non-idempotent actions
  • poor observability
  • customer-visible failures

These are your architectural hotspots.

Step 2: Classify side effects

For each handler, separate:

  • local state mutation
  • primary business side effects
  • secondary side effects
  • externally observable effects
  • compensating effects

This often reveals that one “consumer” is actually several responsibilities stapled together.

Step 3: Introduce local durability and outbox publication

If a handler updates local state and emits events, move to a transactional outbox pattern. Persist state and outgoing messages atomically in the local store, then publish asynchronously. This reduces the classic “DB committed but event not published” failure.

Step 4: Externalize workflow state where needed

If a chain encodes a business process with multiple critical steps, move that logic into an explicit process manager or orchestrated saga. Do not do this for every flow. Do it where process visibility and compensation actually matter.

Step 5: Add reconciliation jobs

This is the part teams skip because it feels unglamorous. It is also the part that makes the migration survive production. Build periodic reconciliation that checks:

  • payments captured without corresponding order state
  • inventory reserved without shipment progression
  • orders stuck in in-between states beyond SLA
  • duplicate external requests
  • missing downstream events

Reconciliation is the mop and bucket of distributed systems. Nobody puts it in the keynote. Everybody needs it.

Step 6: Strangle secondary consumers later

Once the primary flow is safe, separate non-critical consumers—email, analytics, search updates, recommendation engines—so their failures do not contaminate business-critical processing.

Step 7: Tighten event contracts

As the architecture matures, version schemas, define compatibility policies, and publish event ownership clearly. Kafka encourages event reuse; governance prevents semantic vandalism. EA governance checklist

Enterprise Example

Consider a large retailer modernizing its order fulfillment platform. The legacy estate had an ERP, a warehouse management system, a payment gateway integration, and a growing set of microservices around e-commerce. Kafka sat in the middle like a well-meaning traffic officer with no authority. microservices architecture diagrams

Initially, the architecture looked “modern.” The checkout service published OrderSubmitted. Downstream consumers handled payment, fraud screening, stock reservation, fulfillment creation, customer notifications, loyalty updates, and BI feeds. Every team added another consumer because that was easy and politically attractive. Nobody had to ask for orchestration authority.

Then Black Friday happened.

A spike in gateway latency caused payment handlers to retry. The gateway had partial idempotency support, but not for all edge cases. Some orders were double-captured. Meanwhile inventory reservation lagged due to consumer backpressure. Some shipments were created for orders later marked as payment failures because different consumers observed events at different times. Customer support could see pieces of the truth in six systems and the whole truth in none.

The fix was not “more Kafka.” The fix was architectural.

The retailer introduced an Order Fulfillment process manager. Checkout still emitted OrderSubmitted, but critical steps moved under explicit coordination:

  • capture payment
  • evaluate fraud
  • reserve inventory
  • release order to warehouse

Each participating service retained its bounded context and rules. Payment still owned payment semantics. Inventory still owned reservation semantics. The process manager owned workflow progression and timeout policies.

Secondary effects were split out:

  • notifications consumed stable fulfillment events
  • analytics subscribed independently
  • CRM updates were made eventually consistent and non-blocking

They also implemented:

  • idempotency keys for gateway requests
  • outbox publication in payment and inventory services
  • reconciliation jobs comparing order state, payment state, and warehouse release state
  • SLA-based alerts for “stuck in process” orders

The result was not a perfectly pure architecture. There were compromises. The process manager introduced a coordination service that had to be highly available. Some teams disliked the loss of free-form choreography. But the business got something more valuable than purity: orders behaved predictably under stress.

That is what enterprise architecture is for.

Operational Considerations

A design for side effects lives or dies in operations.

Observability

You need traceability across the entire event chain:

  • correlation IDs
  • causation IDs
  • event version
  • handler attempt count
  • idempotency key
  • process instance ID

Without these, debugging turns into folklore.

Distributed tracing helps, but traces alone are not enough for asynchronous systems. You also need durable business state that answers domain questions: “Is this order awaiting payment confirmation or awaiting inventory release?” Logs are not a business model.

Idempotency

Every handler with externally visible side effects should be idempotent or explicitly deduplicated. This includes:

  • payment requests
  • shipment creation
  • email sends if duplicates matter
  • CRM updates
  • customer credits

Idempotency is not just “ignore duplicate message IDs.” It is domain-specific. The right key may be order ID plus action type plus business version.

Backpressure and retries

Retries should be differentiated:

  • transient technical failures: retry with backoff
  • business rejections: do not retry blindly
  • unknown outcomes: move to reconciliation state

A timeout from a payment provider is not the same as a card declined. Treating them the same creates damage.

Dead-letter handling

Dead-letter queues are useful only if someone owns them. A DLQ without triage is a digital basement.

Schema evolution

If events are shared contracts, evolve them carefully. Additive change is safest. Breaking semantic changes disguised as harmless field edits are common and destructive.

Data retention and replay

Kafka replay is powerful, but replaying side effects is dangerous. If you replay a topic into a handler that creates shipments or charges cards, you need a replay-safe mode. Event retention strategy must account for this.

Tradeoffs

There is no free lunch here.

Explicit process management improves visibility and correctness but can reduce local autonomy and add coordination complexity.

Choreography preserves service independence but tends to hide workflow logic in too many places.

Outbox patterns improve consistency between local state and event publication but add operational moving parts.

Reconciliation improves resilience but accepts that the system may be temporarily wrong.

Idempotency reduces duplicate effects but increases state management and design complexity.

And perhaps the biggest tradeoff: semantic discipline slows teams down at first. Naming events carefully, modeling bounded contexts properly, and deciding whether something is a domain event or a command takes time. But the alternative is speed now, confusion forever.

I know which bill I would rather pay.

Failure Modes

These systems fail in recognizable ways. You should name them before they name you.

Duplicate side effects

A message is retried and the handler performs the same external action twice. Classic with payments, shipments, and notifications.

Ghost progression

A downstream service advances workflow based on an event that was later semantically invalidated or compensated.

Lost publication

Local state commits, but the event that should inform other services is never published. The outbox pattern exists largely to prevent this.

Semantic drift

An event originally meant one thing but gradually accumulates consumers interpreting it differently. This is common in large Kafka estates.

Invisible stuck states

The process is waiting on a missing event or failed side effect, but no single system exposes that stuck condition in business terms.

Replay damage

Operational replay re-triggers side effects that were intended to happen once.

Cascading retry storms

A downstream dependency degrades, handlers retry aggressively, consumer lag grows, partitions back up, and the whole chain becomes unstable.

When Not To Use

Event-driven side-effect chains are not the answer to everything.

Do not use this style when:

  • the workflow requires immediate, synchronous user confirmation with tight consistency guarantees
  • the domain is simple enough that a modular monolith would be clearer
  • the number of services is small and team boundaries are not strong
  • failure compensation is impossible or unacceptable
  • the organization lacks operational maturity for observability, replay control, and reconciliation
  • event semantics are poorly understood and likely to churn constantly

In particular, many teams reach for Kafka and microservices long before they have stable domain boundaries. That is like building a highway system before agreeing where the cities are.

A well-structured monolith with explicit domain modules, transactional boundaries, and internal domain events is often the better starting point. You can still apply the same semantic discipline there. In fact, you should.

Several patterns sit close to this problem space.

Transactional Outbox

Ensures local state changes and event publication are coordinated without distributed transactions.

Saga

Coordinates long-running workflows across services, either choreographed or orchestrated.

Process Manager

Tracks workflow state and drives commands based on outcomes.

Inbox / Deduplication Store

Prevents duplicate message handling.

CQRS

Often paired with event-driven systems, especially when read models are updated from events. But CQRS does not solve side effects by itself.

Event Sourcing

Useful in some domains, but often confused with generic event-driven integration. It can help with auditability and replay, but it also sharpens the side-effect problem because projections and process handlers must be replay-safe.

Reconciliation Pattern

Periodic comparison of intended, recorded, and actual state across systems to repair divergence.

These patterns are tools, not identity badges. Use the smallest set that solves your actual problem.

Summary

Side effects in event handlers are where event-driven architecture becomes real. Not fashionable. Real.

The essential discipline is this: treat domain events as facts, treat process decisions as decisions, and do not hide business-critical side effects inside casual consumers. Model primary workflows explicitly. Keep secondary effects isolated. Use local durability and outbox publication. Build idempotency on purpose. Add reconciliation because the network is not honest and third-party systems are not transactional.

Most of all, respect domain semantics. In a healthy architecture, events mean something precise inside bounded contexts. Handlers do not merely react to messages; they carry business intent with clear ownership. That is the difference between a system that scales and a system that merely spreads.

Event-driven systems are powerful precisely because they let change propagate. But propagation without semantic control is just turbulence.

And turbulence is not architecture.

Frequently Asked Questions

What is enterprise architecture?

Enterprise architecture aligns strategy, business processes, applications, and technology in a coherent model. It enables impact analysis, portfolio rationalisation, governance, and transformation planning across the organisation.

How does ArchiMate support architecture practice?

ArchiMate provides a standard language connecting strategy, business operations, applications, and technology. It enables traceability from strategic goals through capabilities and services to infrastructure — making architecture decisions explicit and reviewable.

What tools support enterprise architecture modeling?

The main tools are Sparx Enterprise Architect (ArchiMate, UML, BPMN, SysML), Archi (free, ArchiMate-only), and BiZZdesign. Sparx EA is the most feature-rich, supporting concurrent repositories, automation, scripting, and Jira integration.