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
| Concept | Definition |
|---|---|
| Domain Event | Record of something that happened in the domain |
| Naming | Past tense (OrderPlaced, PaymentReceived) |
| Immutability | Events never change once created |
| Purpose | Decouple 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 │
│ │
└─────────────────────────────────────────────────────────┘