CQRS

What is the CQRS Pattern?

CQRS (Command Query Responsibility Segregation) is a design pattern that separates an application’s read operations (queries) from its write operations (commands). Unlike traditional CRUD models that use the same data model for both reading and writing, CQRS uses distinct models for each:
  • Commands represent operations that change the state of the application (create, update, delete).
  • Queries represent operations that retrieve data without modifying state.
This separation allows for optimized handling of both operations, and the distinct models can evolve independently to meet their specific needs.
flowchart TD Client["Client (UI / API)"] CommandHandler["Command Handler"] WriteModel["Write Model (Domain Logic)"] EventBus["Event Bus / Message Queue"] ReadModelUpdater["Read Model Updater"] ReadModel["Read Model (Optimized for Queries)"] QueryHandler["Query Handler"] Client -->|Send Command| CommandHandler CommandHandler -->|Execute Business Logic| WriteModel WriteModel -->|Publish Events| EventBus EventBus -->|Update Projection| ReadModelUpdater ReadModelUpdater --> ReadModel Client -->|Send Query| QueryHandler QueryHandler -->|Read Optimized Data| ReadModel QueryHandler --> Client

Why Use CQRS?

  • Improved scalability and performance: Read and write workloads can be scaled independently. Queries can be optimized for fast data retrieval, while commands can focus on transactional integrity.
  • Simplified complexity: Complex domains often have different requirements for reads and writes. Separating them avoids overloading a single model with conflicting needs.
  • Better security and maintainability: Different models reduce accidental data exposure and allow teams to isolate concerns.
  • Enables event-driven and asynchronous architectures: Commands can trigger events and processes decoupled from queries.
  • Supports complex business logic: Commands can encapsulate validations and workflows distinct from query logic.
  • Fits well with Domain-Driven Design (DDD): CQRS supports rich domain models on the write side while using simplified read models.

How Does CQRS Work? Key Components

  • Command: A request to perform an action that changes state.
  • Command Handler: Processes commands and performs the action, e.g., updating a database.
  • Query: A request for data without side effects.
  • Query Handler: Retrieves data optimized for specific query needs.
In many implementations, CQRS involves:
  • Separate data models for reads and writes.
  • Different data stores if needed (e.g., a relational DB for writes and a NoSQL DB optimized for reads).
  • An event or messaging system to synchronize data between write and read stores in asynchronous CQRS variants.

Example Implementation of CQRS in .NET

Here is a simplified example demonstrating command and query separation using MediatR (a popular mediator library in .NET often paired with CQRS):
1. Define a Command and Command Handler (to create a product):
// Command public class AddProductCommand : IRequest<bool> { public string Name { get; set; } public decimal Price { get; set; } } // Command Handler public class AddProductCommandHandler : IRequestHandler<AddProductCommand, bool> { private readonly IProductRepository _repository; public AddProductCommandHandler(IProductRepository repository) { _repository = repository; } public async Task<bool> Handle(AddProductCommand request, CancellationToken cancellationToken) { var product = new Product { Name = request.Name, Price = request.Price }; await _repository.AddAsync(product); return true; } }
2. Define a Query and Query Handler (to retrieve product details):
// Query public class GetProductByIdQuery : IRequest<ProductDto> { public int Id { get; set; } } // Query Handler public class GetProductByIdQueryHandler : IRequestHandler<GetProductByIdQuery, ProductDto> { private readonly IProductRepository _repository; public GetProductByIdQueryHandler(IProductRepository repository) { _repository = repository; } public async Task<ProductDto> Handle(GetProductByIdQuery request, CancellationToken cancellationToken) { var product = await _repository.GetByIdAsync(request.Id); return new ProductDto { Id = product.Id, Name = product.Name, Price = product.Price }; } }

Integrating CQRS in .NET Applications

  • Use MediatR to decouple command/query dispatching from handling.
  • Create separate command/query classes and handlers to organize business logic clearly.
  • Register MediatR and handlers in your DI container.
  • Maintain separate models or DTOs for read and write operations.
  • Optional: Use separate databases or read replicas for queries in more advanced architectures.
  • Optionally combine CQRS with Event Sourcing for auditability and event-driven designs.

Benefits and Use Cases of CQRS

Benefits
Use Cases
Scalability (read/write independently)
Complex domains with different read/write needs
Clear separation of concerns
Systems with heavy read traffic vs. write operations
Easier maintenance and testing
Applications using DDD and complex business rules
Supports event-driven architectures
Microservices where eventual consistency is acceptable

Summary Table

Aspect
Description
Purpose
Separate read (query) and write (command) responsibilities for better scalability & clarity
Components
Commands, Command Handlers, Queries, Query Handlers, Optional Event Bus/Message Queues
Benefits
Scalability, easier maintenance, optimized data models, security, and flexibility
Tools
MediatR (for dispatching), Dependency Injection, Separate DTOs/Models
Complexity
Adds architectural complexity so best for complex or large-scale apps

References for Deep Understanding and Examples