# CQRS Pattern A deep dive into the Command Query Responsibility Segregation pattern and how Svrnty.CQRS implements it. ## What is CQRS? CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates **read operations** (queries) from **write operations** (commands). ### Core Principle > "A method should either change the state of an object or return a result, but not both." - Bertrand Meyer (Command-Query Separation) CQRS extends this principle to the architectural level. ## Pattern Components ### Commands (Write Operations) Commands **change system state** but typically don't return data. **Characteristics:** - Imperative naming (CreateUser, PlaceOrder, CancelSubscription) - Contain all data needed for the operation - May return confirmation data (ID, status) - Should be validated - Can fail (validation, business rules) - Not idempotent (usually) **Example:** ```csharp // Command definition public record PlaceOrderCommand { public int CustomerId { get; init; } public List Items { get; init; } = new(); public string ShippingAddress { get; init; } = string.Empty; } // Handler public class PlaceOrderCommandHandler : ICommandHandler { private readonly IOrderRepository _orders; private readonly IInventoryService _inventory; private readonly IEventPublisher _events; public async Task HandleAsync(PlaceOrderCommand command, CancellationToken cancellationToken) { // 1. Validate business rules await ValidateInventory(command.Items); // 2. Change state var order = new Order { CustomerId = command.CustomerId, Items = command.Items, Status = OrderStatus.Pending }; await _orders.AddAsync(order, cancellationToken); // 3. Publish events await _events.PublishAsync(new OrderPlacedEvent { OrderId = order.Id }); // 4. Return result return order.Id; } } ``` ### Queries (Read Operations) Queries **return data** without changing state. **Characteristics:** - Question-based naming (GetOrder, SearchProducts, FetchCustomer) - Never modify state - Always return data - Idempotent (can call multiple times) - Can be cached - Should be fast **Example:** ```csharp // Query definition public record GetOrderQuery { public int OrderId { get; init; } } // DTO (what we return) public record OrderDto { public int Id { get; init; } public string CustomerName { get; init; } = string.Empty; public List Items { get; init; } = new(); public decimal TotalAmount { get; init; } public string Status { get; init; } = string.Empty; } // Handler public class GetOrderQueryHandler : IQueryHandler { private readonly IOrderRepository _orders; public async Task HandleAsync(GetOrderQuery query, CancellationToken cancellationToken) { // 1. Fetch data (no state changes) var order = await _orders.GetByIdAsync(query.OrderId, cancellationToken); if (order == null) throw new KeyNotFoundException($"Order {query.OrderId} not found"); // 2. Map to DTO return new OrderDto { Id = order.Id, CustomerName = order.Customer.Name, Items = order.Items.Select(MapToDto).ToList(), TotalAmount = order.TotalAmount, Status = order.Status.ToString() }; } } ``` ## Benefits of CQRS ### 1. Separation of Concerns **Problem:** Traditional services mix reads and writes: ```csharp public class OrderService { public void CreateOrder(OrderDto dto) { /* write */ } public void UpdateOrder(int id, OrderDto dto) { /* write */ } public void CancelOrder(int id) { /* write */ } public OrderDto GetOrder(int id) { /* read */ } public List SearchOrders(string criteria) { /* read */ } } ``` **Solution:** Separate into commands and queries: ```csharp // Write operations CreateOrderCommandHandler UpdateOrderCommandHandler CancelOrderCommandHandler // Read operations GetOrderQueryHandler SearchOrdersQueryHandler ``` **Benefits:** - Smaller, focused handlers - Single Responsibility Principle - Easier to understand and maintain ### 2. Independent Scaling Scale reads and writes independently: ``` ┌─ Read Replica 1 Read Model (SQL) ───┼─ Read Replica 2 └─ Read Replica 3 Write Model (SQL) ─── Single Writer ``` **Benefits:** - Read-heavy systems can scale reads - Write-heavy systems can optimize writes - Different storage technologies (SQL for writes, NoSQL for reads) ### 3. Optimized Models Different models for different purposes: **Write Model (Normalized):** ```csharp public class Order { public int Id { get; set; } public int CustomerId { get; set; } public Customer Customer { get; set; } = null!; public List Lines { get; set; } = new(); } public class OrderLine { public int Id { get; set; } public int ProductId { get; set; } public Product Product { get; set; } = null!; public int Quantity { get; set; } } ``` **Read Model (Denormalized):** ```csharp public class OrderSummary { public int OrderId { get; set; } public string CustomerName { get; set; } = string.Empty; public string CustomerEmail { get; set; } = string.Empty; public int TotalItems { get; set; } public decimal TotalAmount { get; set; } public string Status { get; set; } = string.Empty; // Pre-computed, optimized for display } ``` **Benefits:** - Write model enforces referential integrity - Read model optimized for queries - No JOIN overhead on reads ### 4. Fine-Grained Security Authorization per command/query: ```csharp public class PlaceOrderAuthorizationService : ICommandAuthorizationService { public Task CanExecuteAsync(PlaceOrderCommand command, ClaimsPrincipal user) { // Only authenticated users can place orders return Task.FromResult(user.Identity?.IsAuthenticated == true); } } public class ViewOrderAuthorizationService : IQueryAuthorizationService { public async Task CanExecuteAsync(GetOrderQuery query, ClaimsPrincipal user) { // Users can only view their own orders (or admins) if (user.IsInRole("Admin")) return true; var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value; var order = await _orders.GetByIdAsync(query.OrderId); return order?.CustomerId.ToString() == userId; } } ``` **Benefits:** - Granular permissions - Different permissions for reads vs writes - Easy to audit ### 5. Testability Handlers are easy to unit test: ```csharp [Fact] public async Task PlaceOrder_WithValidData_ReturnsOrderId() { // Arrange var mockRepository = new Mock(); var handler = new PlaceOrderCommandHandler(mockRepository.Object); var command = new PlaceOrderCommand { CustomerId = 1, Items = new List { new() { ProductId = 1, Quantity = 2 } } }; // Act var result = await handler.HandleAsync(command, CancellationToken.None); // Assert Assert.True(result > 0); mockRepository.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), Times.Once); } ``` ### 6. Event Sourcing Integration CQRS naturally fits with event sourcing: ```csharp public class PlaceOrderCommandHandler : ICommandHandlerWithWorkflow { public async Task HandleAsync(PlaceOrderCommand command, OrderWorkflow workflow, CancellationToken cancellationToken) { // Create order var order = new Order { /* ... */ }; // Emit event (event sourcing) workflow.EmitOrderPlaced(new OrderPlacedEvent { OrderId = order.Id, CustomerId = command.CustomerId, Items = command.Items }); return order.Id; } } ``` ## Trade-offs ### Complexity **Cost:** More files, classes, and concepts to learn. **Mitigation:** - Start simple (single-project structure) - Use templates/code generators - Good naming conventions - Clear documentation ### Code Duplication **Cost:** Similar logic may appear in multiple handlers. **Mitigation:** - Shared services for common logic - Base handler classes (if appropriate) - Domain services - Accept some duplication (prefer clarity over DRY) ### Eventual Consistency **Cost:** With separate read/write models, reads may lag behind writes. **Example:** ```csharp // User places order POST /api/command/placeOrder → Returns 201 Created with orderId: 123 // Immediately query order GET /api/query/getOrder?orderId=123 → May return 404 if read model not updated yet ``` **Mitigation:** - Return complete data from commands - Client-side optimistic updates - Polling/WebSockets for updates - Accept eventual consistency for non-critical data ## CQRS Variants ### 1. Simple CQRS (Svrnty.CQRS Default) Same database, different models: ``` Commands → Write Handlers → Database Queries → Read Handlers → Database (same) ``` **Benefits:** - Simple to understand - No data synchronization - Strong consistency ### 2. CQRS with Read Models Separate read models (views, projections): ``` Commands → Write Handlers → Database (write) ↓ Event Publisher ↓ Projection Handlers ↓ Queries → Read Handlers → Database (read) ``` **Benefits:** - Optimized read models - Different database technologies - Eventual consistency acceptable ### 3. Event-Sourced CQRS Events are the source of truth: ``` Commands → Write Handlers → Event Store (append-only) ↓ Event Subscribers ↓ Projection Handlers ↓ Queries → Read Handlers → Projections (materialized views) ``` **Benefits:** - Complete audit trail - Time-travel debugging - Rebuild projections from events - Event replay ## When to Use CQRS ### ✅ Good Fit 1. **Complex Business Logic** - Many validation rules - Complex workflows - Domain-driven design 2. **Different Read/Write Patterns** - Read-heavy system (10:1 read:write ratio) - Complex queries vs simple writes - Need different optimizations 3. **Audit Requirements** - Track all changes - Who did what when - Compliance (GDPR, SOX, HIPAA) 4. **Scalability Needs** - High read traffic - Geographic distribution - Read replicas 5. **Event-Driven Architecture** - Microservices - Event sourcing - Real-time updates ### ❌ Not Recommended 1. **Simple CRUD** - Basic create/read/update/delete - No complex business logic - Same model for reads and writes 2. **Small Applications** - Few entities - Simple workflows - Small team 3. **Tight Deadlines** - Team unfamiliar with CQRS - Need to ship quickly - Prototype/MVP 4. **Strong Consistency Required** - All reads must reflect latest writes immediately - No eventual consistency acceptable - Simple CQRS (same DB) might work ## Anti-Patterns ### 1. CQRS for Everything **Problem:** Using CQRS for simple CRUD operations. **Solution:** Use CQRS selectively for complex domains. ### 2. Anemic Handlers **Problem:** Handlers with no logic, just calling repository. ```csharp public async Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken) { // ❌ No validation, no business logic return await _repository.AddAsync(new User { Name = command.Name }); } ``` **Solution:** Put business logic in handlers. ```csharp public async Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken) { // ✅ Validation, business rules, events await ValidateUniqueEmail(command.Email); var user = User.Create(command.Name, command.Email); await _repository.AddAsync(user); await _events.PublishAsync(new UserCreatedEvent { UserId = user.Id }); return user.Id; } ``` ### 3. Returning Domain Entities from Queries **Problem:** Exposing internal domain models. ```csharp // ❌ Don't return domain entities public async Task HandleAsync(GetUserQuery query, CancellationToken cancellationToken) { return await _repository.GetByIdAsync(query.UserId); } ``` **Solution:** Always return DTOs. ```csharp // ✅ Return DTOs public async Task HandleAsync(GetUserQuery query, CancellationToken cancellationToken) { var user = await _repository.GetByIdAsync(query.UserId); return new UserDto { Id = user.Id, Name = user.Name, Email = user.Email }; } ``` ### 4. Queries That Modify State **Problem:** Queries with side effects. ```csharp // ❌ Query that changes state public async Task HandleAsync(GetOrderQuery query, CancellationToken cancellationToken) { var order = await _repository.GetByIdAsync(query.OrderId); order.LastViewedAt = DateTime.UtcNow; // ❌ Modifying state! await _repository.UpdateAsync(order); return MapToDto(order); } ``` **Solution:** Keep queries read-only. Use commands for state changes. ```csharp // ✅ Separate command for tracking POST /api/command/trackOrderView { "orderId": 123 } // ✅ Query is read-only GET /api/query/getOrder?orderId=123 ``` ## CQRS in Svrnty.CQRS Svrnty.CQRS implements **Simple CQRS** by default: ```csharp // Commands change state public class CreateUserCommandHandler : ICommandHandler { public async Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken) { // Write to database } } // Queries return data public class GetUserQueryHandler : IQueryHandler { public async Task HandleAsync(GetUserQuery query, CancellationToken cancellationToken) { // Read from database } } ``` **Optional upgrades:** - Event streaming for event-sourced CQRS - Projections for optimized read models - Consumer groups for scalable event processing ## What's Next? - **[Metadata Discovery](metadata-discovery.md)** - How Svrnty.CQRS discovers commands/queries - **[Modular Solution Structure](modular-solution-structure.md)** - Organizing your CQRS application - **[Event Streaming](../event-streaming/README.md)** - Event-sourced CQRS ## See Also - [Getting Started: Introduction](../getting-started/01-introduction.md) - CQRS fundamentals - [Best Practices: Command Design](../best-practices/command-design.md) - Designing effective commands - [Best Practices: Query Design](../best-practices/query-design.md) - Query optimization patterns - [Tutorials: E-Commerce Example](../tutorials/ecommerce-example/README.md) - Real-world CQRS application