plugins/event-modeling/skills/cqrs-architecture/SKILL.md
CQRS pattern implementation and query optimization
npx skillsauth add melodic-software/claude-code-plugins cqrs-architectureInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Design and implement Command Query Responsibility Segregation patterns for scalable systems.
Before implementing CQRS:
docs-management skill for CQRS patternsTraditional 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 │
└───────────────┘ └───────────────┘
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
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
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 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 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 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");
}
}
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
// 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 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
// 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> { }
// 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;
}
}
[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);
}
}
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
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
When implementing CQRS:
For detailed guidance:
Last Updated: 2025-12-26
development
Search Milan Jovanovic's .NET blog for Clean Architecture, DDD, CQRS, EF Core, and ASP.NET Core patterns. Use for finding applicable patterns, code examples, and architecture guidance. Invoke when working with .NET projects that could benefit from proven architectural patterns.
tools
Install and configure Data API Builder (DAB) for production SQL Server MCP access with RBAC
tools
Manage MssqlMcp servers - status, rebuild, and upstream updates
tools
Developer environment setup guides for Windows, macOS, Linux, and WSL. Use when setting up development machines, installing tools, configuring environments, or following platform-specific setup guides. Covers package management, shell/terminal, code editors, AI tooling, containerization, databases, and more.