Table of Contents

Interface IDomainEvent

Namespace
Trellis
Assembly
Trellis.Core.dll

Represents a domain event - a record of something significant that happened in the business domain. Domain events capture state changes and business occurrences that domain experts care about.

public interface IDomainEvent
Extension Methods

Examples

// Define domain events as immutable records with OccurredAt as the timestamp.
// OccurredAt is DateTimeOffset so the authored instant is preserved unambiguously
// through serialization (e.g., outbox tables, integration buses, audit projections).
public record OrderCreated(OrderId OrderId, CustomerId CustomerId, DateTimeOffset OccurredAt) : IDomainEvent;
public record OrderSubmitted(OrderId OrderId, Money Total, DateTimeOffset OccurredAt) : IDomainEvent;

// Raise events from an aggregate. Use TimeProvider.GetUtcNow() (typically injected)
// so tests can pin time deterministically with a fake clock.
public class Order : Aggregate<OrderId>
{
    private readonly TimeProvider _clock;

    private Order(OrderId id, CustomerId customerId, TimeProvider clock) : base(id)
    {
        _clock = clock;
        CustomerId = customerId;
        DomainEvents.Add(new OrderCreated(id, customerId, _clock.GetUtcNow()));
    }

    public Result<Order> Submit()
    {
        return this.ToResult()
            .Ensure(_ => Status == OrderStatus.Draft, Error.UnprocessableContent.ForRule("invalid", "Wrong status"))
            .Tap(_ =>
            {
                Status = OrderStatus.Submitted;
                DomainEvents.Add(new OrderSubmitted(Id, Total, _clock.GetUtcNow()));
            });
    }
}

// Handle the event
public class OrderSubmittedHandler
{
    public async Task Handle(OrderSubmitted evt, CancellationToken ct)
    {
        _logger.LogInformation("Order {OrderId} submitted at {OccurredAt}", 
            evt.OrderId, evt.OccurredAt);
        await _emailService.SendOrderConfirmationAsync(evt.OrderId, ct);
    }
}

Remarks

Domain events are a key tactical pattern in Domain-Driven Design that enable:

  • Loose coupling between aggregates and bounded contexts
  • Temporal decoupling of side effects from core business logic
  • Event sourcing and audit trails
  • Integration between microservices
  • Communication of state changes to external systems

Best practices for domain events:

  • Name events in the past tense (e.g., OrderSubmitted, PaymentProcessed)
  • Make events immutable - use readonly properties or init-only setters
  • Include all relevant data needed by handlers to avoid querying
  • Keep events focused on domain concepts, not technical implementation
  • Use OccurredAt for the event timestamp - avoid redundant timestamp fields

Domain events vs. Integration events:

  • Domain events: Internal to the bounded context, raised by aggregates
  • Integration events: Published across bounded contexts, may be derived from domain events

Properties

OccurredAt

Gets the timestamp when this domain event occurred.

DateTimeOffset OccurredAt { get; }

Property Value

DateTimeOffset

The instant in time when the event was raised, as a DateTimeOffset with explicit UTC offset.

Remarks

This timestamp represents when the business action occurred, not when the event was persisted or published. Author events using GetUtcNow() (typically injected) so the canonical UTC offset (+00:00) is recorded and tests can pin time deterministically with a fake clock.

DateTimeOffset is preferred over DateTime because the offset is an explicit part of the value and round-trips unambiguously through serialization. Events stored in outbox tables, integration buses, and audit projections retain their authored instant without timezone-loss bugs.

Caveat: C# implicitly converts DateTime to DateTimeOffset, so a caller that passes DateTime.Now (Local) or new DateTime(...) (Unspecified, treated as Local) will silently produce an event timestamped with the machine's local offset rather than UTC. The contract does not block this at the type level. Always author events from GetUtcNow() or UtcNow to avoid local-time stamping on non-UTC machines. A static analyzer to flag DateTime sources flowing into OccurredAt may follow in a future release.

Use OccurredAt as the single timestamp for your events - avoid adding redundant fields like CreatedAt, SubmittedAt, etc. that duplicate this information:

// Good - OccurredAt captures when the event happened
public record OrderSubmitted(OrderId OrderId, Money Total, DateTimeOffset OccurredAt) : IDomainEvent;

// Avoid - redundant SubmittedAt duplicates OccurredAt
public record OrderSubmitted(OrderId OrderId, Money Total, DateTimeOffset SubmittedAt, DateTimeOffset OccurredAt) : IDomainEvent;