Skip to content

Domain events

A notification is a message with zero, one, or many handlers and no return value. IDomainEvent is a notification that also records when it happened — the DDD shape.

public record OrderCreatedEvent(Guid OrderId, string UserId) : IDomainEvent
{
public DateTimeOffset OccurredOn { get; } = DateTimeOffset.UtcNow;
}

Handlers

Any number, each independent:

public class SendConfirmationEmail : INotificationHandler<OrderCreatedEvent>
{
public ValueTask Handle(OrderCreatedEvent e, CancellationToken ct) { /* ... */ }
}
public class UpdateInventory : INotificationHandler<OrderCreatedEvent>
{
public ValueTask Handle(OrderCreatedEvent e, CancellationToken ct) { /* ... */ }
}

Publishing

await publisher.Publish(new OrderCreatedEvent(orderId, userId), ct);

Publish is Mediant’s fastest path relative to MediatR — handlers are invoked directly, with no per-handler wrapper object or closure. See Benchmarks.

Strategy

cfg.NotificationPublishStrategy = NotificationPublishStrategy.Sequential; // default
cfg.NotificationPublishStrategy = NotificationPublishStrategy.Parallel;

Sequential runs handlers in order and stops at the first exception. Parallel starts them all and aggregates the failures. Pick Parallel only when handlers are genuinely independent — no shared DbContext, no ordering assumptions.

Polymorphic notifications

cfg.EnablePolymorphicNotifications = true;

Publishing OrderCreatedEvent now also invokes handlers registered for its base types and implemented interfaces — for example an INotificationHandler<IDomainEvent> that writes every domain event to an audit log. Off by default, because it changes which handlers run.

Reliable delivery: the transactional outbox

Publish is in-process and in-memory. If the process dies between committing the transaction and running the handlers, the event is gone. For at-least-once delivery that survives a crash, enqueue the event into the outbox inside your business transaction. A background processor publishes it after the commit and retries failures.

builder.Services.AddMediantOutbox();
public class CreateOrderHandler(AppDbContext db, IOutbox outbox)
: ICommandHandler<CreateOrder, Result<Guid>>
{
public async ValueTask<Result<Guid>> Handle(CreateOrder cmd, CancellationToken ct)
{
var order = Order.Create(cmd.UserId, cmd.Items);
db.Orders.Add(order);
// Enqueued in the same transaction as the row. Both commit, or neither does.
await outbox.EnqueueAsync(new OrderCreatedEvent(order.Id, cmd.UserId), ct);
return Result<Guid>.Success(order.Id);
}
}

Post-commit work

Some work must happen after the transaction commits, but doesn’t need durability — sending an email, warming a cache. Use IPostCommitTaskQueue rather than the outbox:

postCommit.Enqueue(ct => emailService.SendConfirmationAsync(cmd.Email, ct));

The [Transactional] behavior drains the queue once the commit succeeds, and discards it if the transaction rolls back.