8.5 KiB
Introduction to CQRS
Learn what CQRS is, when to use it, and how Svrnty.CQRS implements the pattern.
What is CQRS?
CQRS stands for Command Query Responsibility Segregation. It's an architectural pattern that separates read operations (queries) from write operations (commands).
Traditional Approach
In traditional architectures, the same model handles both reads and writes:
// Traditional approach - same service for everything
public class UserService
{
public void CreateUser(CreateUserDto dto) { /* write */ }
public void UpdateUser(UpdateUserDto dto) { /* write */ }
public UserDto GetUser(int id) { /* read */ }
public List<UserDto> SearchUsers(string criteria) { /* read */ }
}
CQRS Approach
CQRS separates these responsibilities:
// Commands (write operations)
public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, int>
{
public Task<int> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken)
{
// Write logic only
}
}
// Queries (read operations)
public class GetUserQueryHandler : IQueryHandler<GetUserQuery, UserDto>
{
public Task<UserDto> HandleAsync(GetUserQuery query, CancellationToken cancellationToken)
{
// Read logic only
}
}
Core Concepts
Commands
Commands represent write operations that change system state.
Characteristics:
- ✅ Imperative names (CreateUser, UpdateOrder, DeleteProduct)
- ✅ Contain all data needed for the operation
- ✅ May or may not return a result
- ✅ Can be validated before execution
- ✅ Typically have side effects
Example:
public record PlaceOrderCommand
{
public int CustomerId { get; init; }
public List<OrderItem> Items { get; init; } = new();
public decimal TotalAmount { get; init; }
}
Queries
Queries represent read operations that return data without changing state.
Characteristics:
- ✅ Question-based names (GetUser, SearchOrders, FetchProducts)
- ✅ Never modify state
- ✅ Always return data
- ✅ Can be cached
- ✅ Should be idempotent
Example:
public record GetOrderQuery
{
public int OrderId { get; init; }
}
Handlers
Handlers contain the actual business logic for commands and queries.
Command Handler:
public class PlaceOrderCommandHandler : ICommandHandler<PlaceOrderCommand, int>
{
public async Task<int> HandleAsync(PlaceOrderCommand command, CancellationToken cancellationToken)
{
// Validate business rules
// Save to database
// Emit events
// Return order ID
return orderId;
}
}
Query Handler:
public class GetOrderQueryHandler : IQueryHandler<GetOrderQuery, OrderDto>
{
public async Task<OrderDto> HandleAsync(GetOrderQuery query, CancellationToken cancellationToken)
{
// Fetch from database
// Map to DTO
// Return data
return orderDto;
}
}
Why Use CQRS?
Benefits
-
Separation of Concerns
- Commands focus on business logic and validation
- Queries focus on data retrieval and formatting
- Easier to understand and maintain
-
Scalability
- Scale reads and writes independently
- Optimize databases differently (write DB vs read DB)
- Use read replicas for queries
-
Flexibility
- Different models for reading and writing
- Optimize queries without affecting commands
- Easy to add new queries without changing commands
-
Security
- Fine-grained authorization (per command/query)
- Easier to audit write operations
- Clear boundaries for access control
-
Testing
- Handlers are easy to unit test
- Clear inputs and outputs
- Mock dependencies easily
-
Maintainability
- Small, focused handlers
- Single Responsibility Principle
- Easy to add new features
Trade-offs
-
Increased Complexity
- More files and classes
- Learning curve for team
- Might be overkill for simple CRUD
-
Consistency Challenges
- With separate read/write models, eventual consistency may be required
- Requires careful design
-
Code Duplication
- Some logic might be repeated
- More boilerplate code
When to Use CQRS
✅ Good Fit
- Complex business logic - Commands with validation, rules, and workflows
- Different read/write patterns - Complex queries vs simple writes
- High scalability needs - Read-heavy or write-heavy systems
- Audit requirements - Need to track all changes
- Event sourcing - Natural fit with event-driven architectures
- Microservices - Clear boundaries between services
❌ Not Recommended
- Simple CRUD - Basic create/read/update/delete operations
- Small applications - Overhead not justified
- Tight deadlines - Team not familiar with pattern
- Consistent data models - Same model for reads and writes
How Svrnty.CQRS Works
Svrnty.CQRS provides a lightweight, production-ready implementation:
1. Define Commands and Queries
// Just POCOs (Plain Old CLR Objects)
public record CreateProductCommand
{
public string Name { get; init; } = string.Empty;
public decimal Price { get; init; }
}
2. Implement Handlers
public class CreateProductCommandHandler : ICommandHandler<CreateProductCommand, int>
{
private readonly IProductRepository _repository;
public CreateProductCommandHandler(IProductRepository repository)
{
_repository = repository;
}
public async Task<int> HandleAsync(CreateProductCommand command, CancellationToken cancellationToken)
{
var product = new Product { Name = command.Name, Price = command.Price };
await _repository.AddAsync(product, cancellationToken);
return product.Id;
}
}
3. Register in DI
builder.Services.AddCommand<CreateProductCommand, int, CreateProductCommandHandler>();
4. Automatic Endpoint Generation
Svrnty.CQRS automatically creates HTTP or gRPC endpoints:
HTTP:
POST /api/command/createProduct
gRPC:
rpc CreateProduct (CreateProductRequest) returns (CreateProductResponse);
5. Built-in Features
- ✅ Validation - FluentValidation integration
- ✅ Discovery - Metadata-driven endpoint generation
- ✅ Authorization - Custom authorization services
- ✅ Protocols - HTTP (Minimal API) and gRPC support
- ✅ Dynamic Queries - OData-like filtering
- ✅ Event Streaming - Event sourcing and projections
Architecture Overview
┌─────────────────┐
│ HTTP/gRPC │ ← Automatic endpoint generation
│ Endpoints │
└────────┬────────┘
│
┌────────▼────────┐
│ Validation │ ← FluentValidation
│ (Optional) │
└────────┬────────┘
│
┌────────▼────────┐
│ Handler │ ← Your business logic
│ (Command/Query)│
└────────┬────────┘
│
┌────────▼────────┐
│ Data Layer │ ← Database, external APIs, etc.
│ (Your choice) │
└─────────────────┘
Key Principles in Svrnty.CQRS
-
Convention over Configuration
- Minimal setup required
- Automatic endpoint naming
- Sensible defaults
-
Metadata-Driven Discovery
- Handlers registered as metadata
- Runtime enumeration for endpoint generation
- Type-safe at compile time
-
Framework Agnostic
- Works with any data access layer (EF Core, Dapper, etc.)
- No prescribed database or ORM
- Integration points are interfaces
-
Production Ready
- Validation, authorization, observability
- Health checks, metrics, structured logging
- Event sourcing and consumer groups
What's Next?
Now that you understand CQRS, let's get your development environment set up!
Continue to Installation →
See Also
- Architecture: CQRS Pattern - Deeper dive into the pattern
- Architecture: Metadata Discovery - How discovery works
- Best Practices: Command Design - Designing effective commands
- Best Practices: Query Design - Query optimization patterns