Skip to content

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:

  1. UserCreated
  2. AddressChanged
  3. EmailVerified

.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.