Skip to main content

Domain Events: Facts That Happened

Domain Events represent something significant that happened in the domain. They're the bridge between aggregates, enabling loose coupling and eventual consistency. They're also the foundation for Event Sourcing.

TL;DR

ConceptDefinition
Domain EventRecord of something that happened in the domain
NamingPast tense (OrderPlaced, PaymentReceived)
ImmutabilityEvents never change once created
PurposeDecouple aggregates, enable reactions

What Are Domain Events?

Domain events capture facts - things that have already happened:

// Events are facts - past tense, immutable
public record OrderPlaced(
OrderId OrderId,
CustomerId CustomerId,
List<OrderLineInfo> Lines,
Money Total,
DateTime OccurredAt
) : IDomainEvent;

public record PaymentReceived(
PaymentId PaymentId,
OrderId OrderId,
Money Amount,
DateTime OccurredAt
) : IDomainEvent;

Why Domain Events?

1. Decouple Aggregates

Without events:

// ❌ Tight coupling - Order knows about Inventory, Email, Analytics
public class Order
{
public void Place(
IInventoryService inventory,
IEmailService email,
IAnalyticsService analytics)
{
Status = OrderStatus.Placed;
inventory.Reserve(Lines);
email.SendConfirmation(CustomerId);
analytics.TrackOrder(this);
}
}

With events:

// ✅ Loose coupling - Order just emits event
public class Order
{
public void Place()
{
Status = OrderStatus.Placed;
AddDomainEvent(new OrderPlaced(Id, CustomerId, Lines, Total));
// Order doesn't know who reacts
}
}

// Handlers react independently
public class ReserveInventoryHandler : IEventHandler<OrderPlaced> { }
public class SendConfirmationHandler : IEventHandler<OrderPlaced> { }
public class TrackOrderHandler : IEventHandler<OrderPlaced> { }

2. Enable Eventual Consistency

Event Design

Event Structure

public interface IDomainEvent
{
Guid EventId { get; }
DateTime OccurredAt { get; }
}

public abstract record DomainEvent : IDomainEvent
{
public Guid EventId { get; } = Guid.NewGuid();
public DateTime OccurredAt { get; } = DateTime.UtcNow;
}

// Concrete event with all relevant data
public record OrderPlaced(
OrderId OrderId,
CustomerId CustomerId,
List<OrderLineInfo> Lines,
Money Total,
Address ShippingAddress
) : DomainEvent;

What to Include

ALWAYS INCLUDE:
• Aggregate ID (which order?)
• Event ID (for idempotency)
• Timestamp (when did it happen?)
• All data needed by handlers

CONSIDER INCLUDING:
• User/actor who caused the event
• Correlation ID (for tracing)
• Causation ID (what caused this event)

AVOID:
• References to entities (use IDs)
• Sensitive data (passwords, full card numbers)

Emitting and Publishing Events

public class Order : AggregateRoot<OrderId>
{
public void Place()
{
if (Status != OrderStatus.Draft)
throw new InvalidOrderStateException();

Status = OrderStatus.Placed;

// Emit event
AddDomainEvent(new OrderPlaced(
OrderId: Id,
CustomerId: CustomerId,
Lines: Lines.Select(l => new OrderLineInfo(l.ProductId, l.Quantity, l.UnitPrice)).ToList(),
Total: Total,
ShippingAddress: ShippingAddress
));
}
}

// Repository publishes after save
public class OrderRepository : IOrderRepository
{
public async Task Save(Order order)
{
_context.Orders.Update(order);
await _context.SaveChangesAsync();

// Publish events after successful save
foreach (var @event in order.DomainEvents)
{
await _eventPublisher.Publish(@event);
}

order.ClearDomainEvents();
}
}

Common Mistakes

Mistake 1: Events Without Enough Data

// ❌ Not enough data - handlers must query
public record OrderPlaced(OrderId OrderId) : DomainEvent;

// ✅ Self-contained event
public record OrderPlaced(
OrderId OrderId,
CustomerId CustomerId,
string CustomerEmail,
List<OrderLineInfo> Lines,
Money Total
) : DomainEvent;

Mistake 2: Mutable Events

// ❌ Mutable event
public class OrderPlaced : IDomainEvent
{
public OrderId OrderId { get; set; } // Can be changed!
}

// ✅ Immutable event
public record OrderPlaced(OrderId OrderId, Money Total) : IDomainEvent;

Quick Reference Card

┌─────────────────────────────────────────────────────────┐
│ DOMAIN EVENTS QUICK REFERENCE │
├─────────────────────────────────────────────────────────┤
│ │
│ DEFINITION │
│ Record of something significant that happened │
│ │
│ NAMING │
│ • Past tense: OrderPlaced, PaymentReceived │
│ • Domain language, not technical │
│ │
│ STRUCTURE │
│ • Event ID, Timestamp, Aggregate ID │
│ • All data handlers need │
│ │
│ PATTERNS │
│ • Emit from aggregate, publish after save │
│ • Handlers react independently │
│ • Outbox for reliability │
│ │
└─────────────────────────────────────────────────────────┘