Event Sourcing is a design pattern where state changes in a system are stored as a sequence of immutable events rather than updating the current state directly. The current state is reconstructed by replaying these events.
Key Concepts
- Event: A record of something that happened in the system (e.g.,
OrderCreated
,OrderShipped
). - Event Store: A database or storage system where events are persisted.
- Aggregate Root: The main entity that ensures the consistency of changes by applying events.
- Command: An action that changes the state (e.g.,
CreateOrderCommand
). - Event Handler: Listens for and reacts to events.
Benefits of Event Sourcing
- Auditability: Full history of state changes is stored.
- Debugging: Replay events to reproduce bugs.
- Scalability: Easy to distribute events to other systems.
- Flexibility: Rebuild state projections for different views.
- Temporal Queries: Query the state at any point in time.
Disadvantages of Event Sourcing
- Complexity: More components (event store, replay logic).
- Event Versioning: Evolving event schemas is challenging.
- Data Growth: Event storage grows indefinitely.
- Read Model Lag: CQRS projections may lag behind writes.
Detailed C# Implementation
Scenario: An e-commerce system where users can create orders.
1. Domain Event
public interface IDomainEvent
{
DateTime OccurredOn { get; }
}
public class OrderCreatedEvent : IDomainEvent
{
public Guid OrderId { get; }
public string ProductName { get; }
public decimal Price { get; }
public DateTime OccurredOn { get; } = DateTime.UtcNow;
public OrderCreatedEvent(Guid orderId, string productName, decimal price)
{
OrderId = orderId;
ProductName = productName;
Price = price;
}
}
2. Aggregate Root
public abstract class AggregateRoot
{
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void AddDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
public void ClearEvents() => _domainEvents.Clear();
}
public class Order : AggregateRoot
{
public Guid Id { get; private set; }
public string ProductName { get; private set; }
public decimal Price { get; private set; }
private Order() { }
public Order(Guid id, string productName, decimal price)
{
Id = id;
ProductName = productName;
Price = price;
AddDomainEvent(new OrderCreatedEvent(id, productName, price));
}
}
3. Event Store
public interface IEventStore
{
Task SaveEventsAsync(Guid aggregateId, IEnumerable<IDomainEvent> events);
Task<IEnumerable<IDomainEvent>> GetEventsAsync(Guid aggregateId);
}
public class InMemoryEventStore : IEventStore
{
private readonly Dictionary<Guid, List<IDomainEvent>> _store = new();
public async Task SaveEventsAsync(Guid aggregateId, IEnumerable<IDomainEvent> events)
{
if (!_store.ContainsKey(aggregateId))
_store[aggregateId] = new List<IDomainEvent>();
_store[aggregateId].AddRange(events);
await Task.CompletedTask;
}
public async Task<IEnumerable<IDomainEvent>> GetEventsAsync(Guid aggregateId)
{
_store.TryGetValue(aggregateId, out var events);
return await Task.FromResult(events ?? new List<IDomainEvent>());
}
}
4. Command and Handler (CQRS)
public class CreateOrderCommand : IRequest<Guid>
{
public string ProductName { get; }
public decimal Price { get; }
public CreateOrderCommand(string productName, decimal price)
{
ProductName = productName;
Price = price;
}
}
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
private readonly IEventStore _eventStore;
public CreateOrderCommandHandler(IEventStore eventStore)
{
_eventStore = eventStore;
}
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var order = new Order(Guid.NewGuid(), request.ProductName, request.Price);
await _eventStore.SaveEventsAsync(order.Id, order.DomainEvents);
order.ClearEvents();
return order.Id;
}
}
5. Controller
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IMediator _mediator;
public OrdersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand command)
{
var orderId = await _mediator.Send(command);
return Ok(new { OrderId = orderId });
}
}
Flow Explanation
- Client sends a POST request to create an order.
- Controller sends a
CreateOrderCommand
via MediatR. - Command Handler creates the
Order
aggregate and generates anOrderCreatedEvent
. - The Event Store persists the event.
- The client receives the created
OrderId
as feedback.
Advantages in this Implementation
- Auditability: Every order creation is logged as an event.
- Flexibility: Can rebuild order state anytime by replaying events.
- Integration Ready: Can easily publish events to other microservices.
Disadvantages in this Implementation
- Event Versioning: Changing the
OrderCreatedEvent
structure requires careful handling. - Storage Overhead: Events keep growing indefinitely.
- Complexity: More layers and components to manage.