Domain Events vs Integration Events in DDD

⏱ 21 min read

Most event-driven systems don’t fail because Kafka was misconfigured. They fail because the business meaning got smeared somewhere between the aggregate and the topic name. event-driven architecture patterns

That’s the real problem.

Teams say “we publish an event when something happens,” as if all events were the same species. They are not. Some events exist because the domain model needs to express a fact that has already happened inside a bounded context. Others exist because one system needs to tell another system something useful in a durable, interoperable, operationally survivable way. Those are different jobs. Treat them as the same thing and the architecture starts lying.

And once architecture lies, operations pays the bill.

This distinction—domain events versus integration events—looks small on a whiteboard. In production, it decides whether your model remains supple or calcifies into a distributed ball of mud. It decides whether teams can evolve independently, whether migrations stall, whether reconciliation becomes a weekly ceremony, and whether a customer order can be explained three days later under audit.

So let’s be blunt: in Domain-Driven Design, domain events are about domain truth inside a bounded context. Integration events are about communicating across boundaries. They often originate from the same business occurrence. But they are not the same artifact, and they should not be designed the same way.

That separation is one of those architectural moves that feels fussy early and indispensable later.

Context

In a healthy enterprise landscape, business capability is split across bounded contexts: Ordering, Billing, Inventory, Shipping, Customer, Risk, and so on. Each context has its own language, rules, data model, and pace of change. That is classic DDD, but the pressure gets real when those contexts must cooperate in near real time.

A customer places an order. Inventory reserves stock. Payment authorizes. Fulfillment creates a shipment. Analytics wants the fact for reporting. CRM wants to trigger a lifecycle journey. Fraud wants to inspect anomalies. Suddenly one business moment ripples across half a dozen systems.

The instinctive answer in many microservice programs is simple: “emit an event.” Put it on Kafka. Let consumers subscribe. Job done. microservices architecture diagrams

Except it isn’t.

Inside the Ordering context, the meaningful fact may be OrderPlaced. That event is steeped in Ordering’s ubiquitous language. It reflects invariants that were enforced in the aggregate. It may contain value objects meaningful only in that model. It may occur before downstream concerns even exist.

Outside Ordering, the world is different. Shipping does not care about aggregate internals. CRM does not need every field. Finance may require identifiers and monetary amounts with stricter semantics. Data platforms may prefer denormalized facts. Partners may require a versioned contract with explicit compatibility guarantees.

This is where event classification matters. Without it, teams leak internal domain semantics into public integration contracts. They expose what should have remained private, couple consumers to internal model changes, and eventually discover that “event-driven” has quietly become “schema-driven distributed lockstep.”

A bounded context is not just a code package. It is a semantic border. Events crossing that border must respect it.

Problem

The practical problem shows up in a handful of familiar anti-patterns:

  1. A domain event is published directly to Kafka as the enterprise contract.
  2. That feels efficient. It is also usually the first step toward accidental coupling.

  1. Every internal state change becomes an externally visible event.
  2. Consumers start depending on noise, not intent.

  1. Teams serialize aggregates or ORM entities into topics.
  2. Internal representation becomes public API. Refactoring becomes political.

  1. Event names drift from business meaning into technical mush.
  2. OrderUpdated, CustomerChanged, StatusModified. Nobody can tell what happened.

  1. Cross-context consumers infer domain rules from event payloads.
  2. Meaning moves out of the source context and into tribal knowledge.

  1. Migration programs use events to mirror legacy databases without deciding what is domain fact versus replication signal.
  2. Then reconciliation becomes endless.

The result is predictable. Event streams become hard to reason about. Consumers proliferate. Versioning becomes painful. One team’s harmless refactor becomes another team’s outage. Architects start adding governance layers to compensate for a conceptual mistake made at the beginning. EA governance checklist

The mistake is simple: failing to distinguish events that express domain significance from events that serve integration needs.

Forces

This topic matters because several forces pull in opposite directions.

Rich domain semantics vs broad interoperability

A domain event should be rich enough to carry meaningful business intent. It lives close to the model. It should reflect the language experts use.

An integration event should be consumable by outsiders who do not share the same model. It often needs flatter payloads, stable identifiers, explicit versioning, and fewer assumptions.

These are not the same optimization target.

Local consistency vs distributed autonomy

Within a bounded context, a domain event often emerges as part of the same transaction that modified the aggregate. The emphasis is local correctness.

Across contexts, the emphasis shifts to asynchronous delivery, retries, idempotency, ordering guarantees, and contract evolution. The concern is not just “did it happen?” but “can other systems safely act on it later, twice, or out of order?”

Model freedom vs public contract stability

Your domain model should evolve as understanding deepens. That is one of the core promises of DDD.

Your integration contract should evolve slowly and deliberately because many consumers depend on it.

If you collapse the two, you get neither freedom nor stability.

Speed of delivery vs long-term decoupling

Publishing the domain event directly is faster in sprint one.

Separating domain and integration events is slower in sprint one, and faster for the next five years.

Architecture is often the art of paying a small tax now to avoid compound interest later.

Event streaming dreams vs operational reality

Kafka is excellent infrastructure, but Kafka does not solve semantics. It gives you durable logs, partitions, offsets, retention, and consumer groups. It does not tell you which business fact belongs on which topic, what level of granularity is safe, or how to reconcile a failed projection after a partial outage.

The platform is not the design.

Solution

Here is the opinionated answer:

  • Use domain events inside a bounded context to represent business facts that have occurred and matter to the domain model.
  • Use integration events at the boundary to communicate selected facts to other bounded contexts or external systems.
  • Translate from domain event to integration event explicitly.
  • Persist and publish integration events using a reliable boundary mechanism, typically the outbox pattern.

That translation step is not busywork. It is the architectural seam where semantics are curated.

A domain event says, “In Ordering, this happened.”

An integration event says, “For external consumers, here is the contractually stable description of what they need to know.”

Those are different sentences.

Domain events

Domain events are part of the domain model. They should emerge from aggregate behavior, not from CRUD plumbing. They are named in ubiquitous language: OrderPlaced, CreditReserved, ShipmentDispatched.

They are usually:

  • scoped to a bounded context
  • tightly aligned to domain semantics
  • not designed as enterprise-wide public contracts
  • useful for local policies, process managers, internal projections, and audit trails
  • allowed to evolve with the model more freely than external contracts

A good domain event captures a business fact after it becomes true. Not a command. Not an intention. A fact.

Integration events

Integration events are boundary artifacts. Their purpose is interoperability.

They are usually:

  • published to Kafka or another broker for other contexts
  • stable and versioned
  • explicitly shaped for consumer needs without exposing internals
  • resilient to retries, duplicates, and replay
  • accompanied by metadata such as event id, occurred-at timestamp, schema version, correlation id, causation id, tenant, and source system

A good integration event gives consumers enough context to act without forcing them to understand your aggregate design.

The classification model

A practical classification model looks like this:

  • Domain Event: internal business fact
  • Integration Event: externalized business fact for cross-context communication
  • Technical Event: operational or platform signal such as retries, dead-lettering, ingestion completed
  • Data Replication Event: low-semantic change data capture or state synchronization signal

Many enterprises confuse the last two with the first two. That is where event catalogs go to die.

Diagram 1
The classification model

The useful question is not “did something happen?” The useful question is: what kind of happening is this, and who is it for?

Architecture

The architecture that works in real enterprises is usually some variation of this:

  1. A command changes an aggregate inside a bounded context.
  2. The aggregate emits one or more domain events.
  3. The application layer commits state and records the resulting integration event candidates in an outbox.
  4. An outbox publisher publishes integration events to Kafka.
  5. Downstream bounded contexts consume integration events and decide locally what they mean.

That flow preserves local domain purity while giving operations a reliable publication mechanism.

Diagram 2
Architecture

A few important points get missed in simplistic diagrams.

Domain events are not necessarily messages

Inside the same process, a domain event may simply be an in-memory record used to trigger policies or additional behavior after the transaction commits. It does not need Kafka. It does not need Avro. It does not need enterprise governance. ArchiMate for governance

Once the event becomes an integration artifact, the game changes.

Integration events should be consumer-aware, not consumer-specific

This is subtle. You do not design one custom event per consumer. That leads to publishing chaos. But you do design integration events to reflect cross-context needs rather than your internal object graph.

For example, an Ordering context may have an internal OrderPlaced domain event carrying line items as rich value objects, promotion details, decision traces, and aggregate version. The integration event published to Kafka might be OrderAccepted with:

  • orderId
  • customerId
  • acceptedAt
  • currency
  • totalAmount
  • fulfillmentLines
  • deliveryPreference
  • correlationId

Notice the shift. The public event reflects an external business milestone and a stable contract. It does not dump the aggregate’s guts onto the wire.

Event naming matters

Names should describe facts, not generic updates.

Bad:

  • OrderChanged
  • CustomerUpdated
  • InventoryModified

Better:

  • OrderAccepted
  • CustomerEmailVerified
  • StockReserved
  • PaymentAuthorizationCaptured

If the name sounds like a database trigger, the model is already weakening.

Topics are not bounded contexts

Kafka topics are transport structures. They are not your domain decomposition. One bounded context may publish several topics. Several event types may share a topic if retention, partitioning, ordering, and access patterns justify it. Or not.

Do not let broker topology drive the business model.

Migration Strategy

Most enterprises do not get to greenfield this. They start with a legacy estate, perhaps an ERP, a central order management platform, or a monolithic policy administration system. The question is not “how would we design this from scratch?” The question is “how do we move without breaking quarter-end?”

This is where progressive strangler migration earns its keep.

You rarely replace a monolith by cutting all dependencies at once. Instead, you carve bounded contexts out over time. Events become the connective tissue of that migration—but only if you classify them properly.

Step 1: Identify business facts, not tables

Legacy estates often expose CDC streams or table-level changes. That can be useful operationally, but it is not yet domain-driven architecture.

Start by identifying meaningful business facts:

  • order accepted
  • claim adjudicated
  • invoice issued
  • shipment delivered

These become candidates for domain and integration events in the target architecture.

Step 2: Keep CDC as a bridge, not the destination

Debezium and similar CDC tools are excellent for migration. They can mirror state changes from the monolith and feed downstream services during transition. But a row change is not automatically a business event.

Use CDC to buy time. Do not mistake it for the final domain contract.

Step 3: Introduce anti-corruption translation

When the legacy system emits low-semantic changes, introduce a translation layer that maps them into higher-semantic integration events. This is often an anti-corruption layer in DDD terms.

That layer may need enrichment, deduplication, sequence handling, and stateful interpretation. It is not glamorous. It is essential.

Step 4: Carve out one bounded context at a time

Move one capability—say Shipping—behind an event-driven boundary first. Let the legacy order platform continue to own ordering while Shipping subscribes to OrderAccepted integration events. Over time, the new Ordering service can emerge and produce the same integration contract.

That is strangler done properly: consumers are insulated from source replacement because the boundary contract is stable.

Step 5: Add reconciliation from day one

During migration, dual writes, delayed processing, and semantic mismatches are normal. Pretending they won’t happen is amateur hour.

Build reconciliation reports and repair workflows early:

  • source-of-truth versus downstream projection comparison
  • missing event detection
  • duplicate processing detection
  • compensating republish tools
  • business-level discrepancy dashboards

Reconciliation is not a temporary embarrassment. In large enterprises, it is part of the architecture.

Step 5: Add reconciliation from day one
Add reconciliation from day one

A mature migration strategy accepts that there will be a period where truth is operationally distributed, semantically uneven, and occasionally inconsistent. The job of architecture is not to wish that away. The job is to make it survivable.

Enterprise Example

Consider a global retailer modernizing order fulfillment.

The retailer has a monolithic Order Management System managing order capture, payment status, inventory allocation, and shipment orchestration. Every downstream team wants to consume “order events,” so the first proposal is to publish the monolith’s internal order state changes directly to Kafka.

That would be a mistake.

Inside the new Ordering bounded context, an aggregate may emit domain events such as:

  • OrderPlaced
  • PaymentAuthorized
  • OrderConfirmed
  • OrderCancelled

These are useful internally for enforcing policies and driving local workflows.

But externally, other contexts need different semantics.

Shipping needs to know when an order is ready for fulfillment.

Customer Communications needs milestones worth notifying customers about.

Finance needs financially significant facts.

Analytics needs denormalized business facts with stable identifiers.

So the architecture team defines integration events such as:

  • OrderAcceptedForFulfillment
  • OrderReleasedToWarehouse
  • OrderCancellationAccepted
  • RefundInitiated

Notice what happened: the external contract shifted from internal lifecycle details to cross-context commitments.

Why not just publish OrderConfirmed? Because in the domain model, confirmation may occur before fraud review, address validation, or stock reservation completes in all channels. Shipping does not care that Ordering internally considers something “confirmed” if it still cannot pick and pack.

That is the crux: the right integration event is not always the nearest domain event by name.

In this retailer, Kafka carries integration events. Each event includes:

  • immutable eventId
  • eventType
  • schemaVersion
  • occurredAt
  • orderId
  • channel
  • market
  • tenant
  • correlationId
  • business payload

Ordering uses the outbox pattern so that state change and publish intent are committed together. A publisher service then emits to Kafka. Shipping consumes OrderAcceptedForFulfillment and creates its own local shipment aggregate. It does not query Ordering synchronously to understand the event. It acts on the contract.

During migration, legacy OMS continues to produce CDC records. An anti-corruption translator maps those row changes to the same integration events where possible. That allows Shipping to be built once against the future-facing contract, even while the source system changes underneath.

This is where architecture starts earning money. Consumers are protected from source churn. Semantics are explicit. Reconciliation can compare legacy OMS state, outbox records, Kafka offsets, and Shipping projections. Audit and support teams can answer practical questions such as “why did warehouse release not happen for this order?” without spelunking through five databases.

That is not theory. That is what good event classification buys you.

Operational Considerations

Distributed systems fail in boring ways first.

Not elegant CAP-theorem ways. Boring ways: duplicate events, poison messages, clock skew, stale schemas, bad partition keys, replay surprises, consumer lag, and operators who cannot tell whether an event was never published or merely not yet processed.

So design accordingly.

Idempotency is mandatory

Consumers of integration events must assume duplicates. Kafka gives strong delivery mechanics, but “exactly once” is not a magic shield for end-to-end business processing. Downstream side effects still need idempotent handling.

Use event ids, business keys, processed-message tables, or natural idempotency where possible.

Ordering is local, not global

Kafka ordering is typically guaranteed within a partition, not across the whole universe. If event order matters per aggregate or business entity, partition by that key. If consumers need global ordering, challenge the requirement. It is usually a disguised centralized assumption.

Schema evolution needs discipline

Integration events are contracts. Use schema versioning and compatibility rules. Favor additive changes. Avoid reinterpreting existing fields. Keep deprecation periods explicit.

Domain events can evolve faster because they are internal. That is another reason not to publish them directly.

Observability must include business flow

Technical telemetry is not enough. You need:

  • correlation ids across services
  • event lineage
  • business-state dashboards
  • replay auditability
  • dead-letter categorization by business importance

An event platform without business observability is a very fast way to become confused.

Reconciliation is an operating model

Especially in migrations and high-volume enterprises, some discrepancies are inevitable. Build reconciliation as a first-class capability:

  • compare source aggregates and downstream read models
  • detect missing transitions
  • republish safely
  • support business operations in triage

If your architecture assumes no reconciliation, it is not architecture. It is optimism.

Tradeoffs

There is no free lunch here, and pretending otherwise is irresponsible.

Benefit: semantic clarity

Separating domain and integration events preserves model integrity and reduces coupling.

Cost: more design and translation

You now have another layer to own. Teams must think harder about naming, payloads, and boundaries.

Benefit: bounded context autonomy

Internal models can evolve without constantly renegotiating every external consumer.

Cost: latency and complexity

Outbox publishing, translation, and asynchronous delivery add operational moving parts.

Benefit: better migrations

Stable integration contracts decouple consumer rollout from producer replacement.

Cost: duplicate representations

The same business occurrence may appear as one domain event and one or more integration events. Some teams dislike that duplication. They should dislike outages more.

Benefit: stronger governance where it matters

You can apply rigorous schema and lifecycle control to integration events without bureaucratizing all internal domain behavior.

Cost: event catalog sprawl if unmanaged

Without curation, organizations produce too many events with overlapping meanings.

The tradeoff is straightforward: you spend effort at the boundary to avoid system-wide semantic decay.

I would take that deal every time in a serious enterprise.

Failure Modes

The failure modes are remarkably consistent across organizations.

Publishing ORM entities as events

This is the classic sin. Internal persistence shape leaks into public contract. Every refactor becomes a breaking change.

Event names that encode technical updates, not business facts

StatusUpdated creates consumers that must reverse-engineer meaning. That is a modeling failure, not just a naming failure.

One event trying to satisfy every consumer

The payload grows into a kitchen sink. Some consumers rely on fields never intended for them. Contract evolution freezes.

Domain events shared across bounded contexts

What begins as reuse ends as semantic confusion. Different contexts start interpreting the same event differently.

No outbox, only dual write

Database commit succeeds; publish fails. Or publish succeeds; database commit fails. Then everyone debates “eventual consistency” as if it were a philosophy instead of a broken write path.

Treating CDC as business truth

A row mutation is not always a business event. Sometimes it is just a persistence side effect.

No reconciliation path

Missing messages accumulate silently until the month-end close or the regulator asks questions.

Overusing event choreography

Not every business process should emerge from a swarm of loosely coordinated subscribers. Some flows need orchestration, explicit process state, or even a good old-fashioned synchronous call.

Event-driven systems become dangerous when every dependency is hidden in topic subscriptions.

When Not To Use

This distinction is important, but event-driven design is not compulsory architecture.

Do not reach for domain and integration events when:

The domain is simple CRUD with little behavioral richness

If your service mostly stores and retrieves records with minimal business invariants, elaborate domain events may add ceremony without value.

Strong immediate consistency across contexts is truly required

Some workflows cannot tolerate asynchronous lag or compensations. Use transactional boundaries carefully and keep the design simpler.

There are only one or two tightly coupled components

If everything is deployed together, owned by one team, and unlikely to evolve independently, a local modular monolith with method calls may be a better answer.

Consumers really need current query data, not facts

Sometimes a read API is the right integration style. Do not force every interaction into events.

The organization lacks operational maturity

If teams cannot yet handle schema management, replay strategy, idempotency, and incident response in distributed systems, eventing at scale will punish them.

Architecture should fit the organization’s ability to operate it. A Ferrari in a goat field is still a bad transportation plan.

A few patterns sit naturally beside this distinction.

Outbox Pattern

Ensures database state change and integration publish intent are captured atomically, avoiding dual-write failure.

Anti-Corruption Layer

Translates legacy semantics or foreign models into the target bounded context language.

Event-Carried State Transfer

Useful when consumers need enough data in the event to act independently. Dangerous if overdone.

Notification Event

A thin integration event that tells consumers something happened, prompting them to fetch more data. Useful, but can reintroduce synchronous coupling if abused.

Event Sourcing

Sometimes confused with domain events generally. Event sourcing persists state as a sequence of domain events. It is powerful, but not required for using domain events, and certainly not required for integration events.

Process Manager / Saga

Coordinates long-running business processes across contexts. Often consumes integration events and issues commands. Necessary when choreography alone becomes opaque.

CQRS Projections

Domain events often feed internal read models; integration events often feed cross-context projections and analytics pipelines.

The common thread: patterns only help when semantics are explicit. Otherwise they become elaborate machinery around a conceptual blur.

Summary

The line between domain events and integration events is not clerical. It is architectural.

A domain event captures a business fact meaningful inside a bounded context, in the language of that model.

An integration event communicates selected facts across boundaries using a stable, versioned, operationally resilient contract.

They may stem from the same real-world occurrence. They should not usually be the same artifact.

Make that distinction, and several good things happen:

  • bounded contexts keep their semantic integrity
  • public contracts stabilize
  • migrations become tractable
  • Kafka becomes a useful transport rather than a semantic dumping ground
  • reconciliation has something concrete to reconcile
  • teams can evolve with less fear

Ignore it, and the enterprise slowly builds a distributed monolith made of topics instead of tables.

The practical recipe is simple:

  • model domain events from aggregate behavior
  • translate explicitly at the boundary
  • publish integration events via outbox
  • version schemas deliberately
  • design consumers for idempotency and replay
  • treat reconciliation as a first-class capability
  • use strangler migration to move from legacy state changes to meaningful business contracts

In the end, events are not just messages in motion. They are statements about reality. The whole architecture depends on whether those statements mean the right thing to the right audience.

That is the difference between a system that merely emits data and a system that speaks the business truth.

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.