Integration Events
A fact one Module publishes for the others, which each subscribing Module applies in an Operation of its own.
What it is
An Integration Event is a fact a Module publishes for other Modules, describing something that happened. Another Module reacts to it in an Integration Event Handler, in an Operation of its own, and the publishing Operation never waits for it.
Each Integration Event is stored and sent under its wire name, a stable name that carries its version. One wire name is one version of one contract type.
Publish only from a Notification Handler, and never change a published contract
An Integration Event is published by a Notification Handler, after the change it describes has committed. A command handler or an Integration Event Handler never publishes one. A change receivers could not read is a new version: a new type under a new wire name, published beside the old one.
When to use it
- Another Module reacts to what happened in this one: an Integration Event. In the Proving Sample, Inventory opens a stock item and Billing a price list for each product Catalog creates.
- Another Module keeps its own copy of this one's data: a Snapshot instead, as Snapshots and Tombstones explains.
- An Operation cannot go on without an answer from another Module: a question through that Module's Contracts, the one synchronous exception.
How it works
In the Proving Sample, Catalog tells Inventory and Billing about each new product:
CreateProductcommits the product, and itsProductCreatedDomainEventis stored as a Notification in the same commit.- After that commit, Catalog's
PublishProductCreatedNotificationHandlerpublishesProductCreatedV1, which is stored in Catalog's Outbox in the commit of that delivery. - After that commit, Inventory and Billing each apply it in their own Unit of Work, each subscription in a delivery of its own.
The Integration Event is a public record in the publishing Module's Contracts project, with plain values only, and carries the identity and time of the Domain Event it follows from:
/// <summary>
/// A product was added to the catalog. <paramref name="Id"/> and <paramref name="OccurredOn"/> are the identity and the
/// time of the Domain Event it follows from, the same in every delivery. Catalog publishes it under the wire name
/// <c>catalog.product-created.v1</c>; a change other Modules could not read is a new version, not an edit of this one.
/// </summary>
public sealed record ProductCreatedV1(
Guid Id,
DateTimeOffset OccurredOn,
Guid ProductId,
string Name
);The receiving Module holds only the business effect, and the Kit records the Receipt, commits and acknowledges:
// Starts keeping stock of every product Catalog creates, for the tenant the event was delivered for, which GetStockItem
// answers. Only the effect: the Kit records the Receipt, commits and acknowledges, so a redelivered or concurrently
// duplicated ProductCreatedV1 opens one stock item.
internal sealed class OpenStockItemIntegrationEventHandler(
IRepository<StockItem, StockItemId> stockItems,
OperationContext context
) : IIntegrationEventHandler<ProductCreatedV1>
{
public async Task<Result> Handle(
ProductCreatedV1 integrationEvent,
CancellationToken cancellationToken
)
{
await stockItems.AddAsync(
StockItem.Open(integrationEvent.ProductId, integrationEvent.OccurredOn, context.Tenant),
cancellationToken
);
return Result.Success();
}
}The publishing Module declares what it publishes with Publish and the receiving Module what it subscribes to with Subscribe, each in its Composition Root. The receiving project references the publishing Module's Contracts project, never the Module itself. When the publishing Module runs in another Host, the receiving Host declares the wire name with Receive and both Hosts join a broker: see RabbitMQ between Hosts. The Modules do not change.
What the Kit guarantees
| Guarantee | Stated in |
|---|---|
| Stored with the publishing handler's Receipt, one durable delivery per subscription, applied once | What the Kit guarantees |
What Publish refuses | Publishing from a Notification Handler |
| How a new version is published beside the old one | A new version of an Integration Event |
| Wire names and subscription names | Names |
| Across Hosts, over a broker | RabbitMQ between Hosts |
Failure messages
Next steps
Outbox and Notifications
A committed Domain Event waits in its Module's Outbox as a Notification, for the Notification Handlers that react to it outside the Module's own data.
Snapshots and Tombstones
The current state of one aggregate, published for a Module that keeps its own copy of it, and the Tombstone that says it no longer exists.