| .. | ||
| command-design.md | ||
| deployment.md | ||
| error-handling.md | ||
| event-design.md | ||
| multi-tenancy.md | ||
| performance.md | ||
| query-design.md | ||
| README.md | ||
| security.md | ||
| testing.md | ||
Best Practices
Guidelines and patterns for building production-ready applications with Svrnty.CQRS.
Overview
This section provides best practices, design patterns, and recommendations for using Svrnty.CQRS effectively in production environments.
Topics
Command Design
Design effective commands:
- Single responsibility
- Validation at boundaries
- Immutability
- Rich vs anemic models
Query Design
Optimize query performance:
- Projection patterns
- Database indexing
- Caching strategies
- Pagination
Event Design
Event versioning and schema evolution:
- Naming conventions
- Event versioning strategies
- Backward compatibility
- Upcasting patterns
Error Handling
Handle errors gracefully:
- Exception types and mapping
- Validation vs business errors
- Retry strategies
- Dead letter queues
Security
Secure your application:
- Authentication
- Authorization services
- Input validation
- Rate limiting
Performance
Optimize for performance:
- Connection pooling
- Batch operations
- Async/await patterns
- Database tuning
Testing
Test your CQRS application:
- Unit testing handlers
- Integration testing
- Event replay testing
- Projection testing
Deployment
Deploy to production:
- Configuration management
- Database migrations
- Health checks
- Monitoring setup
Multi-Tenancy
Multi-tenant patterns:
- Tenant isolation strategies
- Database per tenant vs shared
- Query filtering
- Security considerations
Quick Reference
Command Best Practices
// ✅ Good
public record CreateOrderCommand
{
[Required]
public int CustomerId { get; init; }
[Required, MinLength(1)]
public List<OrderItem> Items { get; init; } = new();
public decimal TotalAmount => Items.Sum(i => i.Price * i.Quantity);
}
// ❌ Bad
public class CreateOrderCommand
{
public int CustomerId; // Not init-only
public List<OrderItem> Items; // Mutable, no validation
// Missing calculated properties
}
Query Best Practices
// ✅ Good - Projection with pagination
public class ListOrdersQuery
{
public int Page { get; init; } = 1;
public int PageSize { get; init; } = 20;
}
public async Task<PagedResult<OrderSummary>> HandleAsync(ListOrdersQuery query)
{
return await _context.Orders
.AsNoTracking()
.Skip((query.Page - 1) * query.PageSize)
.Take(query.PageSize)
.Select(o => new OrderSummary { ... })
.ToPagedResultAsync();
}
// ❌ Bad - No projection, no pagination
public async Task<List<Order>> HandleAsync()
{
return await _context.Orders.ToListAsync(); // Loads everything!
}
Event Best Practices
// ✅ Good
public record OrderPlacedEvent
{
public string EventId { get; init; } = Guid.NewGuid().ToString();
public int OrderId { get; init; }
public DateTimeOffset PlacedAt { get; init; }
public string CorrelationId { get; init; } = string.Empty;
public int Version { get; init; } = 1; // Versioning from day 1
}
// ❌ Bad
public class OrderPlaced // Present tense
{
public int OrderId; // Mutable
// No event ID, timestamp, or version
}
Architecture Patterns
Modular Solution Structure
Solution/
├── MyApp.Api/ # HTTP/gRPC endpoints
│ ├── Program.cs
│ └── appsettings.json
├── MyApp.CQRS/ # Commands, queries, handlers
│ ├── Commands/
│ ├── Queries/
│ └── Validators/
├── MyApp.Domain/ # Domain models, events
│ ├── Entities/
│ ├── Events/
│ └── ValueObjects/
└── MyApp.Infrastructure/ # Data access, external services
├── Repositories/
├── DbContext/
└── ExternalServices/
Benefits:
- Clear separation of concerns
- Testable in isolation
- Easy to navigate
- Supports team scaling
CQRS + Event Sourcing
Command → Handler → Domain Model → Events → Event Store
↓
Projections → Read Models → Queries
Repository Pattern
public interface IOrderRepository
{
Task<Order> GetByIdAsync(int id);
Task AddAsync(Order order);
Task UpdateAsync(Order order);
}
// Use in command handlers
public class CreateOrderHandler : ICommandHandler<CreateOrderCommand, int>
{
private readonly IOrderRepository _repository;
public async Task<int> HandleAsync(CreateOrderCommand command, CancellationToken ct)
{
var order = Order.Create(command);
await _repository.AddAsync(order);
return order.Id;
}
}
Common Anti-Patterns
❌ Anemic Domain Models
// Bad - Logic in handler, not domain
public class Order
{
public int Id { get; set; }
public decimal Total { get; set; }
public string Status { get; set; }
}
public class PlaceOrderHandler
{
public async Task HandleAsync(PlaceOrderCommand cmd)
{
var order = new Order
{
Total = cmd.Items.Sum(i => i.Price * i.Quantity),
Status = "Placed"
};
// All logic in handler!
}
}
Better:
// Good - Rich domain model
public class Order
{
private readonly List<OrderItem> _items = new();
public static Order Create(List<OrderItem> items)
{
if (items.Count == 0)
throw new InvalidOperationException("Order must have items");
var order = new Order();
order._items.AddRange(items);
return order;
}
public decimal Total => _items.Sum(i => i.Price * i.Quantity);
}
❌ Querying Write Model
// Bad - Querying entity directly
public async Task<Order> GetOrderAsync(int id)
{
return await _context.Orders
.Include(o => o.Items)
.Include(o => o.Customer)
.FirstOrDefaultAsync(o => o.Id == id);
}
Better:
// Good - Use projection/read model
public async Task<OrderSummary> GetOrderAsync(int id)
{
return await _readContext.OrderSummaries
.FirstOrDefaultAsync(o => o.OrderId == id);
}
❌ Missing Validation
// Bad - No validation
public class CreateOrderHandler
{
public async Task<int> HandleAsync(CreateOrderCommand cmd)
{
var order = new Order { CustomerId = cmd.CustomerId }; // What if CustomerId is invalid?
await _repository.AddAsync(order);
return order.Id;
}
}
Better:
// Good - FluentValidation
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator()
{
RuleFor(x => x.CustomerId).GreaterThan(0);
RuleFor(x => x.Items).NotEmpty();
}
}