Skip to main content

Command Palette

Search for a command to run...

Domain Events vs Integration Events in .NET: Which Should Your Team Use?

Updated
โ€ข11 min read
Domain Events vs Integration Events in .NET: Which Should Your Team Use?

Most .NET teams reach for "events" as a general-purpose solution long before they have a clear model for what kind of event they're working with. The confusion is understandable โ€” both domain events and integration events involve something happening and code reacting to it. But they solve completely different problems, live in different layers, and carry different reliability guarantees. Getting them mixed up produces systems that either over-engineer simple flows or silently lose critical cross-service notifications.

The distinction becomes especially important in ASP.NET Core projects built around Clean Architecture or CQRS, where the boundaries between your domain layer, application layer, and external infrastructure must stay sharp. For teams building these patterns in a real production codebase, Chapter 11 and 12 of the ASP.NET Core Web API: Zero to Production course cover exactly this โ€” Clean Architecture, CQRS with MediatR, the Outbox pattern, and domain events all wired together in a full codebase you can run immediately.

ASP.NET Core Web API: Zero to Production

Understanding where your domain ends and your infrastructure begins is what makes the difference between a maintainable codebase and one that bleeds responsibilities across every layer. The patterns covered in this article go much deeper on Patreon, with production-ready source code that shows how domain events dispatch through MediatR pipeline behaviors, how integration events publish via the Outbox, and how the two co-exist cleanly without coupling.

What Are Domain Events?

A domain event represents something that happened within your bounded context โ€” a fact about your domain model. It is raised inside the domain layer, dispatched in-process, and consumed by handlers within the same application boundary. It does not cross a network, does not involve a message broker, and is not durable by default.

The typical shape: an order is placed, an OrderPlaced domain event fires, and handlers within the same process react โ€” perhaps applying a discount policy, checking inventory, or publishing an audit entry. All of this happens synchronously in the same transaction window, or asynchronously via an in-memory dispatcher like MediatR.

Domain events are synchronous in intent even when dispatched asynchronously. Their purpose is to keep your domain model free of direct dependencies on side-effect logic. Instead of your Order entity calling an email service or a notification handler, it raises OrderPlaced and lets the application layer wire the consequences together through handlers.

Key characteristics:

  • Scope: In-process, same bounded context
  • Dispatcher: MediatR INotification + INotificationHandler<T>, or a simple IDomainEventDispatcher
  • Durability: Not durable โ€” if the process crashes after raising the event but before handling, the event is lost
  • Coupling: Handlers are registered via DI โ€” no shared infrastructure required
  • Transaction: Can participate in the same database transaction as the originating aggregate change

What Are Integration Events?

An integration event is a message published across a process boundary โ€” to another service, another bounded context, or any external consumer. It represents a contract between systems, not an internal domain concern.

Where a domain event is a private signal within your model, an integration event is a public broadcast. It travels over a message broker (RabbitMQ, Azure Service Bus, Kafka), persists durably, and must be designed with versioning, idempotency, and consumer compatibility in mind from the start.

The typical shape: after an order is confirmed, the Order service publishes an OrderConfirmed integration event. The Notification service, the Inventory service, and the Analytics service each subscribe independently. They receive the event even if the Order service is briefly offline, because the broker persists it.

Key characteristics:

  • Scope: Cross-process, cross-service, across bounded contexts
  • Transport: Message broker โ€” RabbitMQ, Azure Service Bus, Kafka, MassTransit, NServiceBus
  • Durability: Durable โ€” the broker guarantees delivery even across transient failures
  • Coupling: Consumers depend only on the message contract, not the publishing service's implementation
  • Transaction: Must be handled with the Outbox pattern to guarantee atomic publishing alongside the database write

How Do Domain Events and Integration Events Relate?

The most reliable pattern in production ASP.NET Core systems is: domain events trigger the creation of integration events via the Outbox pattern.

Here's the flow:

  1. An aggregate raises a domain event (OrderPlaced) during the command handler
  2. A domain event handler subscribes to OrderPlaced and writes an OrderConfirmedIntegrationEvent record to the Outbox table โ€” as part of the same SaveChanges call
  3. A background processor reads the Outbox, publishes the integration event to the message broker, and marks the record as processed

This pattern solves the dual-write problem: you can't atomically write to a database and publish to a broker in a single transaction. The Outbox bridges that gap. Your ASP.NET Core Outbox Pattern guide covers the full implementation pattern in detail.

Domain events and integration events do not compete โ€” they compose. Domain events are the internal mechanism for reacting to changes within your model. Integration events are the external mechanism for communicating those changes to the outside world.

Side-by-Side Comparison

Dimension Domain Event Integration Event
Scope In-process, same bounded context Cross-process, cross-service
Transport In-memory (MediatR, custom dispatcher) Message broker (RabbitMQ, Azure Service Bus, Kafka)
Durability Not durable by default Durable โ€” broker persists until consumed
Versioning Not required Required โ€” breaking changes affect all consumers
Idempotency Rarely needed Required โ€” consumers must handle duplicates
Transaction Participates in originating transaction Published via Outbox, separate from domain transaction
Coupling Handlers registered in same process Consumers in separate processes, separate deployments
Failure handling Simple retry via handler Retry, dead-letter queue, poison message handling
Schema Private to bounded context Public contract โ€” treat as a public API

When to Use Domain Events

Use domain events when:

  • You need to react to a state change within your domain model without coupling the aggregate to side-effect logic
  • The reaction happens in the same process and participates in the same unit of work
  • You want to enforce the dependency rule: domain layer raises events, application layer handles them
  • The consequence is immediate and does not need to cross a service boundary

Do not use domain events for: cross-service communication, events that must survive process restarts, or anything that requires a guaranteed delivery contract with external consumers.

When to Use Integration Events

Use integration events when:

  • A state change in one service must notify another service
  • The consuming service is deployed independently and has its own database
  • You need guaranteed delivery โ€” the broker must hold the event until the consumer acknowledges it
  • The event represents a public contract that other teams or services depend on

Do not use integration events for: internal reactions within the same bounded context. Routing everything through a message broker for in-process concerns adds latency, infrastructure complexity, and debuggability overhead for zero gain.

Is It Always Domain โ†’ Integration Events?

Not necessarily. Some teams use integration events without domain events โ€” a command handler writes to the Outbox directly, skipping the domain event layer entirely. This is simpler, and for workflows without a rich domain model, it is often the right call.

Other teams use domain events without integration events โ€” a single-service application where all reactions are in-process and no cross-service communication is needed.

The domain-event-to-integration-event pipeline makes the most sense in systems that: have a genuine domain model with aggregates, use CQRS and Clean Architecture, and communicate with multiple other services. For CRUD APIs or simple services, adding both layers is over-engineering.

What Do Most .NET Teams Get Wrong?

Using integration events in-process. Publishing to a broker for reactions within the same service wastes infrastructure and makes debugging harder. In-process reactions belong to domain events.

Using domain events for cross-service communication. An in-memory event that raises across a service boundary doesn't cross that boundary โ€” it just doesn't execute. Domain events cannot substitute for a message broker.

Skipping the Outbox. Publishing an integration event directly inside a transaction handler (before SaveChanges) creates a dual-write problem. If the broker publish succeeds but SaveChanges fails, the event was never supposed to fire. Always write to the Outbox table inside the same transaction, then publish from a background processor.

Treating integration event schemas as internal. Integration events are contracts. Renaming a property, removing a field, or changing a type without versioning breaks all consumers silently. Apply the same discipline as a REST API: additive changes only, version for breaking changes.

For teams working through the CQRS and MediatR implementation in ASP.NET Core, the point at which domain events should convert to integration events is one of the most frequently misunderstood decisions in Clean Architecture.

Recommendation

For a standard ASP.NET Core API that communicates with at least one other service:

  • Use domain events (via MediatR INotification) for in-process reactions to aggregate state changes
  • Use the Outbox pattern to bridge from domain events to integration events safely
  • Use integration events over a message broker for any cross-service communication
  • Keep the two layers completely separate โ€” integration events should not re-use domain event types

For a single-service application or a simple CRUD API: skip domain events unless you have a genuine domain model. A command handler that writes directly to the Outbox and publishes via a broker is simpler and easier to trace.

โ˜• Building this right takes time. If this walkthrough saved you some, buy us a coffee โ€” every bit helps keep the content coming!

FAQ

What is the difference between a domain event and an integration event in .NET? A domain event is an in-process signal within a bounded context โ€” dispatched via MediatR or a custom dispatcher, not durable, and handled within the same transaction. An integration event is a cross-service message published to a broker (RabbitMQ, Azure Service Bus) โ€” durable, versioned, and consumed by independent services.

Can I use MediatR for integration events in ASP.NET Core? MediatR is in-process only and is not suitable for integration events. It has no built-in broker, no persistence, and no delivery guarantee. Use it for domain events. For integration events, use a message broker with a transport library like MassTransit, NServiceBus, or direct SDK clients for Azure Service Bus or RabbitMQ.

Do I always need both domain events and integration events? No. Use domain events when you have a rich domain model with aggregates and need to keep side-effect logic out of the domain layer. Use integration events when you need to communicate with other services. A simple CRUD API may need neither. A complex microservices system will need both.

How do I prevent losing integration events if my service crashes after committing the database but before publishing to the broker? Use the Outbox pattern: write the integration event to a database table inside the same transaction as your domain change. A background processor polls the Outbox and publishes events to the broker, marking each as processed after acknowledgement. This guarantees at-least-once delivery without dual-write risk.

What is the best way to dispatch domain events in ASP.NET Core? Register domain events as INotification in MediatR and dispatch them from within your command handlers after SaveChangesAsync. You can also dispatch them inside EF Core's SaveChanges override by reading pending events from aggregates before committing. The second approach is cleaner for rich domain models โ€” it ensures events are always dispatched atomically with the persistence operation.

Should integration events use the same types as domain events? No. They serve different purposes and evolve at different rates. Domain event types are internal โ€” they can change freely without affecting consumers. Integration event types are public contracts โ€” changing them requires versioning and backward-compatibility planning. Keep them in separate namespaces and assemblies, with integration events in a shared contracts project if multiple services consume them.

How do I handle failed integration event consumers in ASP.NET Core? Design consumers to be idempotent โ€” processing the same event twice should produce the same outcome. Brokers deliver at-least-once by default. For failures, configure dead-letter queues (DLQ) on the broker so failed messages are captured for inspection rather than dropped. Retry policies with exponential backoff, combined with a DLQ, cover most production failure scenarios.

More from this blog

C

Coding Droplets

219 posts

Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.

From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.