Concepts / Integrating / Domain Events and Integration Events
Integrating

Domain Events and Integration Events

This guide explains the difference between the events a system records for itself and the events it publishes to others. It covers why the two should not be the same by default, how to translate between them, and how to keep published events stable over time.

An event-sourced system records domain events: facts about what happened inside it, in the language of its own model. When other systems need to know about these facts, it is tempting to simply publish the same events. Sometimes that is fine. More often, it ties other systems to details that were never meant to leave.

Two Kinds of Events#

Domain events belong to one system – in terms of Domain-Driven Design, to one bounded context. They describe what happened there, as precisely as the model needs, and they change when the model changes. Nobody outside depends on them, so the team that owns them can refine them freely.

Integration events are what a system tells the outside world. They are published on purpose, to be consumed by systems the publisher does not control. That makes them a contract: once others rely on them, they cannot change without coordination.

The two kinds differ in what they are optimized for:

Domain events Integration events
Audience The system itself Other systems and teams
Granularity As fine as the model needs As coarse as consumers need
Content Everything relevant to the model Only what consumers should rely on
Change Evolves freely with the model Changes rarely, and only in a compatible way

Why Not Publish Everything?#

Publishing domain events directly has an obvious appeal: no translation, no second set of events, and every consumer sees exactly what happened. The price becomes visible later:

  • Internal details leak. A domain event may carry fields that only make sense inside the model, such as internal identifiers, intermediate states, or technical flags. Once published, other systems start to use them.
  • The model freezes. When a team wants to split an event, rename it, or restructure its data, every consumer is affected. Refactoring the model becomes a cross-team negotiation.
  • Consumers carry the complexity. A consumer interested in whether a book is available should not have to reconstruct that from five fine-grained events of the catalog's internal workflow.

Publishing domain events directly is reasonable when producer and consumer are owned by the same team and change together. Across team boundaries, a deliberate set of integration events is usually the better choice.

Translating at the Boundary#

The translation from domain events to integration events happens at the boundary of the producing system. A component observes the domain events and publishes integration events derived from them:

  • It may publish one integration event for one domain event, with a reduced and stable shape.
  • It may publish one integration event for several domain events, summarizing an internal workflow into the fact that matters outside.
  • It may publish nothing for events that are purely internal.

In a library, the catalog might record book-acquired, copy-cataloged, copy-labeled, and copy-shelved as it processes a new book. The rest of the library – the lending system, the website, the reading recommendations – only needs to know one thing: a new copy of a title can now be borrowed. The catalog publishes that as a single integration event, copy-available, and keeps its internal workflow to itself.

In the terms of Domain-Driven Design, the integration events form the published language of a context. The consuming side can protect itself in the same way: an anti-corruption layer translates incoming integration events into its own concepts, so that its model does not depend on the language of another context.

Keeping Integration Events Stable#

Because integration events are a contract, they need the care of a public API:

  • Design for consumers. Publish what consumers need to act on, in terms they understand – not a dump of the producer's state.
  • Evolve compatibly. Adding optional data is usually safe; removing or renaming data is not. When an incompatible change is unavoidable, publish a new version of the event alongside the old one, and retire the old one only when no consumer needs it – see Versioning Events.
  • Describe them. Document every integration event: what it means, when it is published, what its data contains, and which version is current. Schemas make this checkable.
  • Use a common envelope. A standard format for metadata such as type, source, and time helps consumers treat events from different producers the same way – see CloudEvents.

Integration Events in an Event-Sourced System#

An event-sourced system has an advantage when it comes to integration: all domain events are already recorded, in order, and can be read at any time. The translation to integration events can therefore run as an ordinary event handler that observes the domain events and publishes their external counterparts – and if a new integration event is introduced later, it can be derived for the entire history, not just from now on.

Whether integration events are stored in the same event store, in a separate one, or only passed on to a broker, is a design decision. What matters is that publishing them is reliable – see Reliable Delivery.

Drawing the Line#

Keep domain events free to follow the model, and make integration events a deliberate, stable promise to the outside. The line between them is the line between what a system knows and what it shares – and drawing it consciously is what allows systems to evolve independently while still working together.