Cross-Context Integration Events
Relevant source files
The following files were used as context for generating this wiki page:
- Shared/IntegrationEvents/Scheduling/PatientCreatedIntegrationEvent.cs
- BuildingBlocks/BuildingBlocks.Application/Messaging/IIntegrationEvent.cs
- BuildingBlocks/BuildingBlocks.Infrastructure.EfCore/EfCoreUnitOfWork.cs
- Core/Scheduling/Scheduling.Application/Patients/EventHandlers/PatientCreatedEventHandler.cs
- Core/Scheduling/Scheduling.Infrastructure/Persistence/Migrations/20260327124204_AddOutboxTables.Designer.cs
- claude.md
Cross-Context Integration Events facilitate asynchronous communication between different bounded contexts (such as Scheduling and Billing) within the system. Unlike Domain Events, which are internal to a specific bounded context, Integration Events serve as a public contract, allowing services to react to state changes in other parts of the system without tight coupling.
The implementation relies on an Outbox pattern to ensure reliable message delivery, utilizing a message broker like RabbitMQ and a service bus abstraction such as MassTransit. This architecture ensures that domain state changes and the corresponding integration events are committed atomically within a single transaction.
Sources: BuildingBlocks.Application/Messaging/IIntegrationEvent.cs:1-12, claude.md:46-55
Architectural Components
The integration event system is composed of several key layers and abstractions that handle the lifecycle of an event from creation to delivery.
1. Event Contracts
Integration events are defined in a shared project to be accessible by both producers and consumers. They implement the IIntegrationEvent marker interface, which mandates an EventId and an OccurredOn timestamp.
| Property | Type | Description |
|---|---|---|
EventId | Guid | Unique identifier for the specific event instance. |
OccurredOn | DateTime | The UTC timestamp when the event was generated. |
Sources: BuildingBlocks.Application/Messaging/IIntegrationEvent.cs:13-24, Shared/IntegrationEvents/Scheduling/PatientCreatedIntegrationEvent.cs:1-15
2. The Outbox Pattern
To prevent data inconsistency where a database update succeeds but a message publish fails (or vice versa), the system uses an Outbox. Integration events are first written to the Scheduling_OutboxMessage table within the same database transaction as the domain data.
Event Lifecycle and Data Flow
The flow begins with a Domain Event and ends with an Integration Event being published to the broker.
Step 1: Domain Event Handling
When a domain aggregate root changes state, it raises a Domain Event. An internal INotificationHandler (via MediatR) catches this event and translates it into an Integration Event.
public Task Handle(PatientCreatedEvent notification, CancellationToken cancellationToken)
{
// Translate internal domain data to public integration contract
_unitOfWork.QueueIntegrationEvent(new PatientCreatedIntegrationEvent(
notification.PatientId,
notification.FirstName,
notification.LastName,
notification.Email,
notification.DateOfBirth));
return Task.CompletedTask;
}
Sources: Core/Scheduling/Scheduling.Application/Patients/EventHandlers/PatientCreatedEventHandler.cs:22-35
Step 2: Unit of Work Processing
The EfCoreUnitOfWork manages the queue of integration events. During SaveChangesAsync, these events are moved to the persistence layer's outbox.
- Queueing: Events are added to an internal
_queuedIntegrationEventslist. - Publishing to Outbox: Reflection is used to call the
IEventBus.PublishAsync<T>method for each queued event, which writes it to theOutboxMessagetable. - Commit: The transaction is finalized. If the transaction fails, the
_queuedIntegrationEventslist is cleared to prevent accidental publishing.
Sources: BuildingBlocks.Infrastructure.EfCore/EfCoreUnitOfWork.cs:81-112, BuildingBlocks.Infrastructure.EfCore/EfCoreUnitOfWork.cs:146-160
Sequence of Operations
The following diagram illustrates the interaction between the application layer, the Unit of Work, and the persistence outbox during a save operation.
Persistence Schema
The infrastructure includes specific tables to manage the inbox and outbox states, ensuring idempotency and "at-least-once" delivery.
| Table Name | Purpose | Key Fields |
|---|---|---|
Scheduling_OutboxMessage | Stores outgoing integration events. | SequenceNumber, Body, MessageType, SentTime |
Scheduling_OutboxState | Tracks delivery status of outbox messages. | OutboxId, Delivered, LastSequenceNumber |
Scheduling_InboxState | Tracks consumed messages to prevent duplicate processing. | MessageId, ConsumerId, Consumed |
Summary
Cross-Context Integration Events are a critical part of the system's asynchronous communication strategy. By implementing the Outbox pattern within the EfCoreUnitOfWork, the project ensures that cross-service messages (like PatientCreatedIntegrationEvent) are only published if the primary database transaction succeeds. This provides a robust foundation for building eventual consistency across Bounded Contexts such as Scheduling and Billing.
Sources: claude.md:46-55, BuildingBlocks.Infrastructure.EfCore/EfCoreUnitOfWork.cs:150-165