Skip to content

Domain Events Specification

This is the API specification for domain event related public types provided by the Functorium framework. For design principles and implementation patterns, see the Domain Events Guide.

TypeNamespaceDescription
IDomainEventFunctorium.Domains.EventsDomain event base interface (INotification extension)
DomainEventFunctorium.Domains.EventsDomain event base abstract record (immutability, value equality)
IHasDomainEventsFunctorium.Domains.EventsRead-only marker interface for tracking Aggregate events
IDomainEventDrainFunctorium.Domains.EventsEvent cleanup interface (internal, infrastructure only)
IDomainEventCollectorFunctorium.Applications.EventsCollects events by tracking Aggregates within Scoped scope
IDomainEventPublisherFunctorium.Applications.EventsDomain event publisher interface (FinT return)
IDomainEventHandler<TEvent>Functorium.Applications.EventsDomain event handler interface (INotificationHandler extension)
PublishResultFunctorium.Applications.EventsMultiple event publishing result (partial success/failure tracking)
ObservableDomainEventPublisherFunctorium.Adapters.EventsIDomainEventPublisher observability decorator
ObservableDomainEventNotificationPublisherFunctorium.Adapters.EventsINotificationPublisher implementation providing handler-perspective observability
IUsecaseCtxEnricher<TRequest, TResponse>Functorium.Abstractions.ObservabilitiesEnricher that adds business context fields to Usecase logs
IDomainEventCtxEnricher<TEvent>Functorium.Abstractions.ObservabilitiesEnricher that adds business context fields to domain event handler logs
CtxEnricherContextFunctorium.Abstractions.ObservabilitiesStatic utility that manages LogContext Push factory
CtxRootAttributeFunctorium.Abstractions.ObservabilitiesSpecifies fields to promote to ctx root level in source generators
CtxIgnoreAttributeFunctorium.Applications.UsecasesExcludes from CtxEnricher auto-generation target in source generators

Event Contract (IDomainEvent, DomainEvent)

Section titled “Event Contract (IDomainEvent, DomainEvent)”

The base interface for domain events. Extends Mediator’s INotification to provide Pub/Sub integration.

namespace Functorium.Domains.Events;
public interface IDomainEvent : INotification
{
DateTimeOffset OccurredAt { get; }
Ulid EventId { get; }
string? CorrelationId { get; }
string? CausationId { get; }
}
PropertyTypeDescription
OccurredAtDateTimeOffsetEvent occurrence time
EventIdUlidUnique event identifier (for deduplication and tracking)
CorrelationIdstring?Request tracking ID (traces events from the same request)
CausationIdstring?Cause event ID (ID of the preceding event that triggered this event)

The base abstract record for domain events. Provides immutability and value-based equality.

namespace Functorium.Domains.Events;
public abstract record DomainEvent(
DateTimeOffset OccurredAt,
Ulid EventId,
string? CorrelationId,
string? CausationId) : IDomainEvent
{
protected DomainEvent()
: this(DateTimeOffset.UtcNow, Ulid.NewUlid(), null, null) { }
protected DomainEvent(string? correlationId)
: this(DateTimeOffset.UtcNow, Ulid.NewUlid(), correlationId, null) { }
protected DomainEvent(string? correlationId, string? causationId)
: this(DateTimeOffset.UtcNow, Ulid.NewUlid(), correlationId, causationId) { }
}
ConstructorDescription
DomainEvent()Creates with current time and new EventId (CorrelationId and CausationId are null)
DomainEvent(string? correlationId)Creates with specified CorrelationId
DomainEvent(string? correlationId, string? causationId)Creates with specified CorrelationId and CausationId

A read-only marker interface for tracking Aggregates that have domain events.

namespace Functorium.Domains.Events;
public interface IHasDomainEvents
{
IReadOnlyList<IDomainEvent> DomainEvents { get; }
}
PropertyTypeDescription
DomainEventsIReadOnlyList<IDomainEvent>Domain event list registered in the Aggregate (read-only)

An infrastructure interface for removing Aggregate events after publishing. Separated from the domain contract (IHasDomainEvents) to explicitly designate event cleanup as an infrastructure concern.

namespace Functorium.Domains.Events;
internal interface IDomainEventDrain : IHasDomainEvents
{
void ClearDomainEvents();
}
MethodReturn TypeDescription
ClearDomainEvents()voidRemoves all domain events

Access level: internal. Do not call directly from application code. AggregateRoot<TId> implements this interface, and infrastructure code (Publisher) automatically calls it after publishing.

// Defined as nested records within the Aggregate
public class Order : AggregateRoot<OrderId>
{
public sealed record CreatedEvent(OrderId OrderId, Money TotalAmount) : DomainEvent;
public sealed record ConfirmedEvent(OrderId OrderId) : DomainEvent;
public static Order Create(Money totalAmount)
{
var id = OrderId.New();
var order = new Order(id, totalAmount);
order.AddDomainEvent(new CreatedEvent(id, totalAmount));
return order;
}
}

An interface that collects domain events by tracking Aggregates within a Scoped scope. Track() is called in Repository Create/Update, and events are collected via GetTrackedAggregates() in UsecaseTransactionPipeline.

namespace Functorium.Applications.Events;
public interface IDomainEventCollector
{
void Track(IHasDomainEvents aggregate);
void TrackRange(IEnumerable<IHasDomainEvents> aggregates);
IReadOnlyList<IHasDomainEvents> GetTrackedAggregates();
void TrackEvent(IDomainEvent domainEvent);
IReadOnlyList<IDomainEvent> GetDirectlyTrackedEvents();
}
MethodReturn TypeDescription
Track(IHasDomainEvents aggregate)voidRegisters an Aggregate as a tracking target (ignored if already registered)
TrackRange(IEnumerable<IHasDomainEvents> aggregates)voidBatch registers multiple Aggregates as tracking targets
GetTrackedAggregates()IReadOnlyList<IHasDomainEvents>Returns tracked Aggregates that have domain events
TrackEvent(IDomainEvent domainEvent)voidDirectly tracks bulk events created by Domain Services
GetDirectlyTrackedEvents()IReadOnlyList<IDomainEvent>Directly tracks bulk events created by Domain Services
  1. In Repository, call IDomainEventCollector.Track(aggregate) during Create/Update
  2. In UsecaseTransactionPipeline, after SaveChanges, query Aggregates with events via GetTrackedAggregates()
  3. Publish collected events via IDomainEventPublisher

The domain event publisher interface. Uses the same FinT return pattern as Repository/Port.

namespace Functorium.Applications.Events;
public interface IDomainEventPublisher
{
FinT<IO, Unit> Publish<TEvent>(
TEvent domainEvent,
CancellationToken cancellationToken = default)
where TEvent : IDomainEvent;
FinT<IO, Seq<PublishResult>> PublishTrackedEvents(
CancellationToken cancellationToken = default);
}
MethodReturn TypeDescription
Publish<TEvent>(TEvent, CancellationToken)FinT<IO, Unit>Publishes a single domain event
PublishTrackedEvents(CancellationToken)FinT<IO, Seq<PublishResult>>Publishes events from all Aggregates tracked by IDomainEventCollector and clears them

Generic constraint: TEvent must implement IDomainEvent.

A result record that tracks partial success/failure when publishing multiple events.

namespace Functorium.Applications.Events;
public sealed record PublishResult(
Seq<IDomainEvent> SuccessfulEvents,
Seq<(IDomainEvent Event, Error Error)> FailedEvents)
{
public bool IsAllSuccessful { get; }
public bool HasFailures { get; }
public int TotalCount { get; }
public int SuccessCount { get; }
public int FailureCount { get; }
public static PublishResult Empty { get; }
public static PublishResult Success(Seq<IDomainEvent> events);
public static PublishResult Failure(Seq<(IDomainEvent Event, Error Error)> failures);
}
PropertyTypeDescription
SuccessfulEventsSeq<IDomainEvent>List of successfully published events
FailedEventsSeq<(IDomainEvent Event, Error Error)>List of failed events and their errors
IsAllSuccessfulboolWhether all events were successfully published (FailedEvents.IsEmpty)
HasFailuresboolWhether there are any failed events
TotalCountintTotal number of published events
SuccessCountintNumber of successful events
FailureCountintNumber of failed events
Factory MethodReturn TypeDescription
EmptyPublishResultEmpty result (no events)
Success(Seq<IDomainEvent>)PublishResultResult where all events succeeded
Failure(Seq<(IDomainEvent, Error)>)PublishResultResult where all events failed

Event Handler (IDomainEventHandler<TEvent>)

Section titled “Event Handler (IDomainEventHandler<TEvent>)”

The domain event handler interface. Extends Mediator’s INotificationHandler<TEvent> to provide source generator compatibility.

namespace Functorium.Applications.Events;
public interface IDomainEventHandler<in TEvent> : INotificationHandler<TEvent>
where TEvent : IDomainEvent
{
// Inherited from INotificationHandler<TEvent>:
// ValueTask Handle(TEvent notification, CancellationToken cancellationToken)
}

Generic constraint: TEvent must implement IDomainEvent.

Inherited MethodReturn TypeDescription
Handle(TEvent notification, CancellationToken cancellationToken)ValueTaskProcesses domain events (inherited from INotificationHandler<TEvent>)
public sealed class OnOrderCreated : IDomainEventHandler<Order.CreatedEvent>
{
public async ValueTask Handle(Order.CreatedEvent notification, CancellationToken cancellationToken)
{
// Handle side effects such as inventory deduction, notification dispatch, etc.
}
}

An IDomainEventPublisher decorator with integrated observability (logging, tracing, metrics). Provides observability for event publishing in the Adapter Layer.

namespace Functorium.Adapters.Events;
public sealed class ObservableDomainEventPublisher : IDomainEventPublisher, IDisposable
{
public ObservableDomainEventPublisher(
ActivitySource activitySource,
IDomainEventPublisher inner,
IDomainEventCollector collector,
ILogger<ObservableDomainEventPublisher> logger,
IMeterFactory meterFactory,
IOptions<OpenTelemetryOptions> openTelemetryOptions);
public FinT<IO, Unit> Publish<TEvent>(
TEvent domainEvent,
CancellationToken cancellationToken = default)
where TEvent : IDomainEvent;
public FinT<IO, Seq<PublishResult>> PublishTrackedEvents(
CancellationToken cancellationToken = default);
public void Dispose();
}
Constructor ParameterTypeDescription
activitySourceActivitySourceActivitySource for distributed tracing (DI injected)
innerIDomainEventPublisherActual publisher to decorate
collectorIDomainEventCollectorCollector for calculating tracked event counts
loggerILogger<ObservableDomainEventPublisher>Logger
meterFactoryIMeterFactoryMeter factory
openTelemetryOptionsIOptions<OpenTelemetryOptions>OpenTelemetry configuration

Observability items:

ItemName PatternDescription
Meter{ServiceNamespace}.adapter.eventAdapter Layer event Meter
Counter (Request)adapter.event.requestsEvent publish request count
Counter (Response)adapter.event.responsesEvent publish response count
Histogram (Duration)adapter.event.durationEvent publish processing time (seconds)

ObservableDomainEventNotificationPublisher

Section titled “ObservableDomainEventNotificationPublisher”

An INotificationPublisher implementation that provides handler-perspective observability (logging, tracing, metrics) for domain event handlers.

namespace Functorium.Adapters.Events;
public sealed class ObservableDomainEventNotificationPublisher : INotificationPublisher, IDisposable
{
public ObservableDomainEventNotificationPublisher(
ActivitySource activitySource,
ILoggerFactory loggerFactory,
IMeterFactory meterFactory,
IOptions<OpenTelemetryOptions> openTelemetryOptions,
IServiceProvider serviceProvider);
public ValueTask Publish<TNotification>(
NotificationHandlers<TNotification> handlers,
TNotification notification,
CancellationToken cancellationToken)
where TNotification : INotification;
public void Dispose();
}
Constructor ParameterTypeDescription
activitySourceActivitySourceActivitySource for distributed tracing (DI injected)
loggerFactoryILoggerFactoryFactory for creating per-handler Loggers
meterFactoryIMeterFactoryMeter factory
openTelemetryOptionsIOptions<OpenTelemetryOptions>OpenTelemetry configuration
serviceProviderIServiceProviderDI container for resolving IDomainEventCtxEnricher

Behavior:

  • Applies observability only to Notifications that are IDomainEvent.
  • Notifications that are not IDomainEvent are published via default ForeachAwait without observability.
  • Before handler processing, resolves IDomainEventCtxEnricher<TEvent> from DI and automatically pushes custom properties to LogContext.

Observability items:

ItemName PatternDescription
Meter{ServiceNamespace}.applicationApplication Layer Meter
Counter (Request)application.usecase.event.requestsHandler request count
Counter (Response)application.usecase.event.responsesHandler response count
Histogram (Duration)application.usecase.event.durationHandler processing time (seconds)

Mediator 3.0 constraint: Mediator 3.0 does not support IPipelineBehavior for INotification, and the source generator directly uses concrete types rather than the INotificationPublisher interface. Therefore, Scrutor’s Decorate pattern does not work, and NotificationPublisherType configuration must be used.

services.AddMediator(options =>
{
options.NotificationPublisherType = typeof(ObservableDomainEventNotificationPublisher);
});
// RegisterDomainEventHandlersFromAssembly scans for handlers.

Ctx Enricher (IUsecaseCtxEnricher, IDomainEventCtxEnricher)

Section titled “Ctx Enricher (IUsecaseCtxEnricher, IDomainEventCtxEnricher)”

The interface for enrichers that add business context fields to Usecase logs. The built-in UsecaseLoggingPipeline automatically pushes custom properties to LogContext when outputting Request/Response logs.

namespace Functorium.Abstractions.Observabilities;
public interface IUsecaseCtxEnricher<in TRequest, in TResponse>
where TResponse : IFinResponse
{
IDisposable? EnrichRequestLog(TRequest request);
IDisposable? EnrichResponseLog(TRequest request, TResponse response);
}
MethodReturn TypeDescription
EnrichRequestLog(TRequest request)IDisposable?Push properties to LogContext before Request log output
EnrichResponseLog(TRequest request, TResponse response)IDisposable?Push properties to LogContext before Response log output

Generic constraint: TResponse must implement IFinResponse.

The interface for enrichers that add business context fields to domain event handler logs. ObservableDomainEventNotificationPublisher automatically pushes custom properties to LogContext during Handler processing.

namespace Functorium.Abstractions.Observabilities;
public interface IDomainEventCtxEnricher<in TEvent> : IDomainEventCtxEnricher
where TEvent : IDomainEvent
{
IDisposable? EnrichLog(TEvent domainEvent);
}
public interface IDomainEventCtxEnricher
{
IDisposable? EnrichLog(IDomainEvent domainEvent);
}
InterfaceMethodDescription
IDomainEventCtxEnricher<TEvent>EnrichLog(TEvent domainEvent)Type-safe event log Enrichment
IDomainEventCtxEnricher (non-generic)EnrichLog(IDomainEvent domainEvent)Bridge interface used for runtime type resolution and invocation

Implementation rule: IDomainEventCtxEnricher(non-generic) directly. IDomainEventCtxEnricher<TEvent> automatically provides the non-generic bridge via Default Interface Method.

Generic constraint: TEvent must implement IDomainEvent.

A static utility class that manages the LogContext Push factory. Serves as a bridge connecting LogContext.PushProperty from logging frameworks like Serilog to the framework.

namespace Functorium.Abstractions.Observabilities;
public static class CtxEnricherContext
{
public static void SetPushPropertyFactory(Func<string, object?, IDisposable> factory);
public static IDisposable PushProperty(string name, object? value);
}
MethodReturn TypeDescription
SetPushPropertyFactory(Func<string, object?, IDisposable>)voidSet LogContext Push factory (called once at application startup)
PushProperty(string name, object? value)IDisposablePush property to LogContext with specified name and value

Initialization: If the factory is not set, PushProperty returns a no-op NullDisposable.


Source Generator Attributes ([CtxRoot], [CtxIgnore])

Section titled “Source Generator Attributes ([CtxRoot], [CtxIgnore])”

An attribute that instructs the source generator to promote the field to ctx root level (ctx.{field}) when generating CtxEnricher.

namespace Functorium.Abstractions.Observabilities;
[AttributeUsage(
AttributeTargets.Interface | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class CtxRootAttribute : Attribute;
TargetBehavior
InterfaceThe field is promoted to ctx.{field} in all Request/Response implementing this interface
PropertyThe property is promoted to ctx.{field}
ParameterThe record constructor parameter is promoted to ctx.{field}

An attribute that instructs the source generator to exclude from CtxEnricher auto-generation target.

namespace Functorium.Applications.Usecases;
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class CtxIgnoreAttribute : Attribute;
TargetBehavior
ClassThe entire Request record is excluded from CtxEnricher auto-generation
PropertyThe property is excluded from CtxEnricher auto-generation
ParameterThe record constructor parameter is excluded from CtxEnricher auto-generation
// Apply [CtxRoot] to interface — promoted in all implementing Requests
[CtxRoot]
public interface IHasOrderId
{
OrderId OrderId { get; }
}
// Apply to individual properties
public sealed record CreateOrderCommand(
[property: CtxRoot] OrderId OrderId, // Promoted to ctx.OrderId
[property: CtxIgnore] string Payload, // Excluded from Enricher
Money TotalAmount) : ICommandRequest<CreateOrderResponse>;