CQRS & Event Sourcing: High-Scale Data Architecture
CQRS & Event Sourcing: High-Scale Data Architecture
CQRS (Command Query Responsibility Segregation) and Event Sourcing are often used together to build highly scalable, auditable, and performant systems.
🏗️ 1. CQRS (Deep Dive)
CQRS separates the “write” operations (Commands) from the “read” operations (Queries). This allows you to scale them independently.
- Command Side: Optimized for business logic and consistency.
- Query Side: Optimized for fast data retrieval (often using a flat “Read Model” or a NoSQL database like Elasticsearch).
🚀 .NET Implementation (using MediatR)
// 1. The Command (Write)
public record CreateOrderCommand(Guid ProductId, int Quantity) : IRequest<Guid>;
// 2. The Query (Read)
public record GetOrderSummaryQuery(Guid OrderId) : IRequest<OrderSummary>;
// 3. The Handler (Read Side - Optimized for speed)
public class GetOrderSummaryHandler : IRequestHandler<GetOrderSummaryQuery, OrderSummary>
{
private readonly IDbConnection _dapper; // Use Dapper for fast reads!
public async Task<OrderSummary> Handle(...) =>
await _dapper.QuerySingleAsync<OrderSummary>("SELECT * FROM OrderSummaries WHERE Id = @Id", ...);
}🚀 2. Event Sourcing
Instead of storing the current state of an object, you store the sequence of events that led to that state.
🏗️ The Problem
In a traditional database, if a user changes their address, you overwrite the old address. You lose the history of when and why it changed.
🛠️ The Solution (The “Event Store”)
Every change is an immutable event:
UserCreatedAddressChangedEmailVerified
.NET Event Model
public record OrderPlaced(Guid OrderId, decimal TotalAmount, DateTime PlacedAt);
public record OrderShipped(Guid OrderId, DateTime ShippedAt);
// To get the current state, you "REPLAY" the events
public class OrderAggregate
{
public string Status { get; private set; }
public void Apply(OrderPlaced e) => Status = "Placed";
public void Apply(OrderShipped e) => Status = "Shipped";
}💡 Why use them together?
- Auditing: You have a perfect log of every single change ever made.
- Temporal Querying: You can reconstruct the state of the system at any point in the past (“What did the user’s profile look like 2 weeks ago?”).
- Scalability: You can have multiple different “Read Models” (one for the UI, one for Reporting, one for Search) all updated by the same event stream.