PouryaKit
Concepts

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:

  1. CreateProduct commits the product, and its ProductCreatedDomainEvent is stored as a Notification in the same commit.
  2. After that commit, Catalog's PublishProductCreatedNotificationHandler publishes ProductCreatedV1, which is stored in Catalog's Outbox in the commit of that delivery.
  3. 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:

ProductCreatedV1.cs
/// <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:

OpenStockItemIntegrationEventHandler.cs
// 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

GuaranteeStated in
Stored with the publishing handler's Receipt, one durable delivery per subscription, applied onceWhat the Kit guarantees
What Publish refusesPublishing from a Notification Handler
How a new version is published beside the old oneA new version of an Integration Event
Wire names and subscription namesNames
Across Hosts, over a brokerRabbitMQ between Hosts

Failure messages

Next steps

On this page