| name | cqrs-architecture |
| description | CQRS pattern implementation and query optimization |
| allowed-tools | Read, Glob, Grep, Write, Edit |
CQRS Architecture Skill
Design and implement Command Query Responsibility Segregation patterns for scalable systems.
MANDATORY: Documentation-First Approach
Before implementing CQRS:
- Invoke
docs-managementskill for CQRS patterns - Verify patterns via MCP servers (perplexity, context7)
- Base guidance on established CQRS literature
CQRS Fundamentals
Traditional vs CQRS:
TRADITIONAL (Single Model):
┌─────────────────────────────────┐
│ Application │
├─────────────────────────────────┤
│ Domain Model │
│ (Reads + Writes) │
├─────────────────────────────────┤
│ Database │
└─────────────────────────────────┘
CQRS (Separated Models):
┌───────────────┐ ┌───────────────┐
│ Command Side │ │ Query Side │
│ (Write Model) │ │ (Read Model) │
├───────────────┤ ├───────────────┤
│ Domain Logic │ │ DTO/Views │
│ Aggregates │ │ Projections │
├───────────────┤ ├───────────────┤
│ Write DB │───►│ Read DB │
└───────────────┘ └───────────────┘
CQRS Levels
Level 1: Logical Separation
Same database, separate code paths:
┌─────────────────────────────────────┐
│ Application │
├──────────────────┬──────────────────┤
│ Command Handlers │ Query Handlers │
│ - Validation │ - Direct SQL │
│ - Domain Logic │ - Projections │
│ - Events │ - DTOs │
├──────────────────┴──────────────────┤
│ Single Database │
└─────────────────────────────────────┘
Benefits:
✓ Clean separation in code
✓ Simple deployment
✓ Single source of truth
✓ Good starting point
Level 2: Separate Read Models
Same write DB, separate read DB:
┌─────────────────┐ ┌─────────────────┐
│ Command Side │ │ Query Side │
├─────────────────┤ ├─────────────────┤
│ Command Handler │ │ Query Handler │
│ Domain Model │ │ DTOs │
├─────────────────┤ ├─────────────────┤
│ Write Database │───►│ Read Database │
│ (Normalized) │sync│ (Denormalized) │
└─────────────────┘ └─────────────────┘
Benefits:
✓ Optimized read performance
✓ Scale reads independently
✓ Different storage technologies
✓ Eventually consistent reads
Level 3: Event-Sourced CQRS
Event store as write model, projections as read:
┌─────────────────┐ ┌─────────────────┐
│ Command Side │ │ Query Side │
├─────────────────┤ ├─────────────────┤
│ Command Handler │ │ Query Handler │
│ Aggregate │ │ Read Models │
├─────────────────┤ ├─────────────────┤
│ Event Store │───►│ Multiple Read │
│ (Append-only) │ │ Databases │
└─────────────────┘ └─────────────────┘
Benefits:
✓ Complete audit trail
✓ Temporal queries
✓ Multiple projections
✓ Rebuild read models
Command Side Design
Command Structure
// Command Definition
public record PlaceOrderCommand(
Guid CustomerId,
List<OrderItemDto> Items,
string ShippingAddress
) : ICommand<OrderId>;
// Command Handler
public class PlaceOrderHandler : ICommandHandler<PlaceOrderCommand, OrderId>
{
private readonly IOrderRepository _repository;
private readonly IEventPublisher _events;
public async Task<OrderId> HandleAsync(
PlaceOrderCommand command,
CancellationToken ct)
{
// Validation
if (!command.Items.Any())
throw new ValidationException("Order must have items");
// Domain logic
var order = Order.Create(
command.CustomerId,
command.Items.Select(i => new OrderItem(i.ProductId, i.Quantity)));
// Persistence
await _repository.SaveAsync(order, ct);
// Publish events
await _events.PublishAsync(order.GetDomainEvents(), ct);
return order.Id;
}
}
Command Patterns
Command Best Practices:
NAMING:
- Imperative: PlaceOrder, CancelOrder, UpdateAddress
- Include context: not just "Create" but "CreateOrder"
STRUCTURE:
- Immutable (records)
- Only data needed for operation
- No business logic in command
VALIDATION:
- Input validation in handler
- Business validation in domain
- Return meaningful errors
IDEMPOTENCY:
- Include idempotency key
- Handle duplicate submissions
- Return same result for retries
Query Side Design
Query Structure
// Query Definition
public record GetOrderByIdQuery(Guid OrderId) : IQuery<OrderDetailsDto>;
// Query Handler
public class GetOrderByIdHandler : IQueryHandler<GetOrderByIdQuery, OrderDetailsDto>
{
private readonly IReadDbContext _db;
public async Task<OrderDetailsDto> HandleAsync(
GetOrderByIdQuery query,
CancellationToken ct)
{
var order = await _db.OrderDetails
.Where(o => o.OrderId == query.OrderId)
.Select(o => new OrderDetailsDto
{
OrderId = o.OrderId,
CustomerName = o.Customer.Name,
Items = o.Items.Select(i => new OrderItemDto
{
ProductName = i.ProductName,
Quantity = i.Quantity,
Price = i.Price
}).ToList(),
Status = o.Status,
TotalAmount = o.TotalAmount
})
.FirstOrDefaultAsync(ct);
return order ?? throw new NotFoundException("Order not found");
}
}
Read Model Optimization
Query Optimization Strategies:
1. DENORMALIZATION
- Pre-join data
- Store calculated values
- Flatten hierarchies
2. MATERIALIZED VIEWS
- Database-managed
- Automatically updated
- Query-optimized
3. CACHING
- In-memory for hot data
- Distributed for shared
- Invalidate on events
4. SPECIALIZED STORES
- ElasticSearch for search
- Redis for real-time
- ClickHouse for analytics
Synchronization Patterns
Projection from Events
// Event-Driven Projection
public class OrderProjection : IEventHandler<OrderPlaced>, IEventHandler<OrderShipped>
{
private readonly IOrderViewRepository _views;
public async Task HandleAsync(OrderPlaced @event, CancellationToken ct)
{
var view = new OrderView
{
OrderId = @event.OrderId,
CustomerId = @event.CustomerId,
Status = "Placed",
PlacedAt = @event.Timestamp,
ItemCount = @event.Items.Count,
TotalAmount = @event.TotalAmount
};
await _views.InsertAsync(view, ct);
}
public async Task HandleAsync(OrderShipped @event, CancellationToken ct)
{
await _views.UpdateAsync(@event.OrderId, view =>
{
view.Status = "Shipped";
view.ShippedAt = @event.Timestamp;
view.TrackingNumber = @event.TrackingNumber;
}, ct);
}
}
Consistency Patterns
Consistency Options:
STRONG CONSISTENCY (Same Transaction):
┌──────────┐ ┌──────────┐
│ Command │───►│ Read │
│ DB │ │ Model │
│ │ │ Update │
└──────────┴────┴──────────┘
Same Transaction
EVENTUAL CONSISTENCY (Async):
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Command │───►│ Message │───►│ Read │
│ DB │ │ Queue │ │ Model │
└──────────┘ └──────────┘ └──────────┘
Async, Eventually Consistent
HYBRID (Read-Your-Writes):
- Immediate read from command side
- Eventually consistent for others
- Version checking in queries
MediatR Implementation
Setup with MediatR
// Registration
services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
});
// Command/Query Interfaces
public interface ICommand<TResult> : IRequest<TResult> { }
public interface IQuery<TResult> : IRequest<TResult> { }
// Handler Interfaces
public interface ICommandHandler<TCommand, TResult>
: IRequestHandler<TCommand, TResult>
where TCommand : ICommand<TResult> { }
public interface IQueryHandler<TQuery, TResult>
: IRequestHandler<TQuery, TResult>
where TQuery : IQuery<TResult> { }
Pipeline Behaviors
// Validation Behavior
public class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
var failures = _validators
.Select(v => v.Validate(request))
.SelectMany(r => r.Errors)
.Where(f => f != null)
.ToList();
if (failures.Any())
throw new ValidationException(failures);
return await next();
}
}
// Logging Behavior
public class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
_logger.LogInformation("Handling {RequestType}", typeof(TRequest).Name);
var response = await next();
_logger.LogInformation("Handled {RequestType}", typeof(TRequest).Name);
return response;
}
}
API Design with CQRS
REST API Pattern
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
private readonly IMediator _mediator;
// Commands use POST/PUT/DELETE
[HttpPost]
public async Task<ActionResult<OrderId>> PlaceOrder(
[FromBody] PlaceOrderCommand command,
CancellationToken ct)
{
var orderId = await _mediator.Send(command, ct);
return CreatedAtAction(nameof(GetOrder), new { id = orderId }, orderId);
}
// Queries use GET
[HttpGet("{id}")]
public async Task<ActionResult<OrderDetailsDto>> GetOrder(
Guid id,
CancellationToken ct)
{
var order = await _mediator.Send(new GetOrderByIdQuery(id), ct);
return Ok(order);
}
[HttpGet]
public async Task<ActionResult<PagedResult<OrderSummaryDto>>> ListOrders(
[FromQuery] ListOrdersQuery query,
CancellationToken ct)
{
var orders = await _mediator.Send(query, ct);
return Ok(orders);
}
}
When to Use CQRS
Good Fit
CQRS Works Well For:
✓ Complex reads AND writes
- Different optimization needs
- Read/write ratio imbalance
✓ Multiple views of data
- Different query patterns
- Multiple UI requirements
✓ Collaborative domains
- Many concurrent users
- Complex validation
✓ Event-driven systems
- Microservices
- Async processing
✓ Scalability requirements
- Independent read/write scaling
- Performance optimization
Poor Fit
CQRS May Not Fit:
✗ Simple CRUD applications
- Overhead not justified
- Same model works fine
✗ Small team/project
- Added complexity
- Maintenance burden
✗ Strong consistency required
- Real-time requirements
- Financial transactions
✗ Unknown query patterns
- Ad-hoc reporting
- BI requirements
Workflow
When implementing CQRS:
- Evaluate Fit: Is CQRS appropriate for this context?
- Choose Level: Logical, physical, or event-sourced?
- Design Commands: Identify write operations
- Design Queries: Identify read patterns
- Plan Sync: How will read models be updated?
- Implement Pipeline: Validation, logging, etc.
- Consider Consistency: What guarantees are needed?
- Test Both Sides: Command and query testing
References
For detailed guidance:
Last Updated: 2025-12-26