Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CQRS (Command Query Responsibility Segregation)

Overview

CQRS là một architectural pattern tách biệt operations đọc (read/queries) và ghi (write/commands) thành hai models riêng biệt. Điều này cho phép tối ưu hóa mỗi side một cách độc lập về performance, scalability, và security.

Basic Concept

Traditional Approach:
┌─────────────────────────────────────┐
│          Single Model               │
│  ┌─────────────┐   ┌─────────────┐  │
│  │   Read      │   │   Write     │  │
│  │   (Queries) │   │   (Commands)│  │
│  └─────────────┘   └─────────────┘  │
└─────────────────────────────────────┘

CQRS Approach:
┌─────────────────────────────────────┐
│     Command Side      │  Query Side │
│  ┌─────────────────┐ │ ┌─────────┐  │
│  │  Domain Model   │ │ │  Read   │  │
│  │  (Write Model) │ │ │  Model  │  │
│  └─────────────────┘ │ └─────────┘  │
└─────────────────────────────────────┘

Command vs Query

AspectCommandQuery
PurposeModify stateRead data
Returnvoid/ResultData
Side EffectsYesNo
idempotentUsually notYes
// Commands - Change state
public class CreateOrderCommand : ICommand
{
    public Guid CustomerId { get; set; }
    public List<OrderItemDto> Items { get; set; }
}

public class UpdateOrderStatusCommand : ICommand
{
    public Guid OrderId { get; set; }
    public OrderStatus NewStatus { get; set; }
}

// Queries - Read data
public class GetOrderByIdQuery : IQuery<OrderDto>
{
    public Guid OrderId { get; set; }
}

public class GetCustomerOrdersQuery : IQuery<List<OrderSummaryDto>>
{
    public Guid CustomerId { get; set; }
}

Implementation

1. Models

// Write Model - Domain-focused
public class Order
{
    public Guid Id { get; private set; }
    public Guid CustomerId { get; private set; }
    public List<OrderItem> Items { get; private set; }
    public OrderStatus Status { get; private set; }
    
    public void AddItem(Product product, int quantity)
    {
        // Domain logic
    }
    
    public void Place()
    {
        // Business rules
        Status = OrderStatus.Placed;
    }
}

// Read Model - UI/Presentation-focused
public class OrderDto
{
    public Guid Id { get; set; }
    public string CustomerName { get; set; }
    public List<OrderItemDto> Items { get; set; }
    public decimal TotalAmount { get; set; }
    public string StatusDisplay { get; set; }
    public string FormattedOrderDate { get; set; }
}

public class OrderSummaryDto  // Lightweight for lists
{
    public Guid Id { get; set; }
    public string CustomerName { get; set; }
    public decimal TotalAmount { get; set; }
    public string Status { get; set; }
}

2. Command Handler

public interface ICommandHandler<TCommand> where TCommand : ICommand
{
    Task<Result> HandleAsync(TCommand command);
}

public class CreateOrderHandler : ICommandHandler<CreateOrderCommand>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IEventBus _eventBus;
    
    public CreateOrderHandler(
        IOrderRepository orderRepository,
        IEventBus eventBus)
    {
        _orderRepository = orderRepository;
        _eventBus = eventBus;
    }
    
    public async Task<Result> HandleAsync(CreateOrderCommand command)
    {
        // Validation
        var validationResult = await _validator.ValidateAsync(command);
        if (!validationResult.IsValid)
            return Result.Failure(validationResult.Errors);
        
        // Create order
        var order = new Order(command.CustomerId);
        
        foreach (var item in command.Items)
        {
            var product = await _productRepository.GetByIdAsync(item.ProductId);
            order.AddItem(product, item.Quantity);
        }
        
        // Save
        await _orderRepository.AddAsync(order);
        
        // Publish event
        await _eventBus.PublishAsync(new OrderCreatedEvent(order));
        
        return Result.Success(order.Id);
    }
}

3. Query Handler

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    Task<TResult> HandleAsync(TQuery query);
}

public class GetOrderByIdHandler : IQueryHandler<GetOrderByIdQuery, OrderDto>
{
    private readonly IOrderReadRepository _readRepository;
    
    public GetOrderByIdHandler(IOrderReadRepository readRepository)
    {
        _readRepository = readRepository;
    }
    
    public async Task<OrderDto> HandleAsync(GetOrderByIdQuery query)
    {
        return await _readRepository.GetByIdAsync(query.OrderId);
    }
}

public class GetCustomerOrdersHandler : IQueryHandler<GetCustomerOrdersQuery, List<OrderSummaryDto>>
{
    private readonly IOrderReadRepository _readRepository;
    
    public async Task<List<OrderSummaryDto>> HandleAsync(GetCustomerOrdersQuery query)
    {
        return await _readRepository.GetByCustomerIdAsync(query.CustomerId);
    }
}

4. Mediator Pattern for Decoupling

// In ASP.NET Core
public class OrdersController : ControllerBase
{
    private readonly IMediator _mediator;
    
    public OrdersController(IMediator mediator)
    {
        _mediator = mediator;
    }
    
    [HttpPost]
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand command)
    {
        var result = await _mediator.Send(command);
        
        if (result.IsSuccess)
            return Created($"/orders/{result.Value}", result.Value);
            
        return BadRequest(result.Error);
    }
    
    [HttpGet("{id}")]
    public async Task<OrderDto> GetOrder(Guid id)
    {
        return await _mediator.Send(new GetOrderByIdQuery { OrderId = id });
    }
}

5. Read Database

// Separate read database (optional - can be same DB with different projection)
public interface IOrderReadRepository
{
    Task<OrderDto> GetByIdAsync(Guid id);
    Task<List<OrderSummaryDto>> GetByCustomerIdAsync(Guid customerId);
    Task<List<OrderSummaryDto>> GetRecentOrdersAsync(int count);
}

public class OrderReadRepository : IOrderReadRepository
{
    private readonly ReadDbContext _context;
    
    public async Task<OrderDto> GetByIdAsync(Guid id)
    {
        return await _context.Orders
            .Where(o => o.Id == id)
            .Select(o => new OrderDto
            {
                Id = o.Id,
                CustomerName = o.Customer.Name,
                Items = o.Items.Select(i => new OrderItemDto
                {
                    ProductName = i.Product.Name,
                    Quantity = i.Quantity,
                    UnitPrice = i.UnitPrice
                }).ToList(),
                TotalAmount = o.TotalAmount,
                StatusDisplay = o.Status.ToString(),
                FormattedOrderDate = o.CreatedAt.ToString("dd/MM/yyyy")
            })
            .FirstOrDefaultAsync();
    }
}

Synchronization (Keeping Read/Write in Sync)

Option 1: Synchronous Updates

public class CreateOrderHandler
{
    public async Task<Result> HandleAsync(CreateOrderCommand command)
    {
        // Write to main database
        var order = new Order(command.CustomerId);
        await _writeRepository.AddAsync(order);
        
        // Immediately update read database
        await _readRepository.InsertAsync(MapToReadModel(order));
        
        return Result.Success(order.Id);
    }
}

Option 2: Event-Based (Async)

// Write to event store
public class CreateOrderHandler
{
    public async Task<Result> HandleAsync(CreateOrderCommand command)
    {
        var order = new Order(command.CustomerId);
        await _orderRepository.AddAsync(order);
        
        // Publish event
        await _eventBus.PublishAsync(new OrderCreatedEvent(order));
    }
}

// Event handler updates read database
public class OrderCreatedEventHandler : IEventHandler<OrderCreatedEvent>
{
    private readonly IOrderReadRepository _readRepository;
    
    public async Task HandleAsync(OrderCreatedEvent evt)
    {
        var readModel = new OrderDto
        {
            Id = evt.Order.Id,
            // ... map fields
        };
        
        await _readRepository.InsertAsync(readModel);
    }
}

Benefits

BenefitDescription
Independent ScalingScale reads and writes separately
PerformanceOptimized query models for reads
FlexibilityDifferent data stores for different purposes
SecurityEasier to secure write operations
ComplexityComplex domain logic isolated in commands

Challenges

ChallengeDescription
ComplexityMore moving parts than simple CRUD
Consistencyeventual consistency between read/write
Learning CurveTeam needs to understand pattern
OverheadMay be overkill for simple apps

When to Use

  • Complex domains với nhiều business rules
  • High read-to-write ratio applications
  • Applications cần different read và write models
  • Systems cần high scalability for reads
  • Event-driven architectures

When NOT to Use

  • Simple CRUD applications
  • Low complexity domains
  • Teams mới vào CQRS
  • Projects cần rapid development

Comparison

AspectCRUDCQRS
ModelSingle modelSeparate models
ComplexityLowMedium-High
ScalabilityLimitedHigh
ConsistencyImmediateEventual
Best forSimple appsComplex domains

References