Command Query Separation (CQS)
The Principle
A method should either be a Command that performs an action, or a Query that returns data, but not both.
┌─────────────────────────────────────────────────────────┐
│ METHOD │
├───────────────────────┬─────────────────────────────────┤
│ COMMAND │ QUERY │
├───────────────────────┼─────────────────────────────────┤
│ - Changes state │ - Returns data │
│ - Returns void │ - No side effects │
│ - "Do something" │ - "Tell me something" │
│ - Imperative verbs │ - Noun or question │
│ (Create, Update, │ (Get, Find, Is, Has, │
│ Delete, Process) │ Calculate, Count) │
└───────────────────────┴─────────────────────────────────┘
CQS at Method Level
Violation Examples
// BAD: Method both modifies state AND returns data
public class Stack<T>
{
public T Pop() // Both removes item AND returns it
{
var item = _items[_count - 1];
_count--;
return item;
}
}
// BAD: Getter with side effects
public class Counter
{
private int _value;
public int Value
{
get { return _value++; } // Query that modifies state!
}
}
// BAD: Command returns data
public class UserService
{
public User CreateUser(string email, string name) // Returns created user
{
var user = new User { Email = email, Name = name };
_repository.Add(user);
return user; // CQS violation
}
}
Correct CQS Implementation
// GOOD: Separated commands and queries
public class Stack<T>
{
// Query - returns data, no side effects
public T Peek()
{
if (_count == 0)
throw new InvalidOperationException("Stack is empty");
return _items[_count - 1];
}
// Command - modifies state, returns void
public void Pop()
{
if (_count == 0)
throw new InvalidOperationException("Stack is empty");
_count--;
}
// Usage: separate calls
var top = stack.Peek();
stack.Pop();
}
// GOOD: Counter with pure query
public class Counter
{
private int _value;
public int Value => _value; // Pure query
public void Increment() => _value++; // Command
}
// GOOD: User service with CQS
public class UserService
{
// Command - creates user, returns identifier only
public Guid CreateUser(string email, string name)
{
var userId = Guid.NewGuid();
var user = new User { Id = userId, Email = email, Name = name };
_repository.Add(user);
return userId; // Returning ID is acceptable
}
// Query - retrieves user
public User? GetUser(Guid userId)
{
return _repository.GetById(userId);
}
}
CQS Exceptions
Some situations justify combining command and query:
1. Atomic Operations
// Acceptable: Interlocked operations need to be atomic
public int IncrementAndGet()
{
return Interlocked.Increment(ref _counter);
}
// Acceptable: Compare-and-swap patterns
public bool TryUpdate(int expected, int newValue)
{
return Interlocked.CompareExchange(ref _value, newValue, expected) == expected;
}
2. Fluent APIs
// Acceptable: Builder pattern returns this
public class QueryBuilder
{
public QueryBuilder Where(string condition)
{
_conditions.Add(condition);
return this; // Returns modified builder
}
public QueryBuilder OrderBy(string column)
{
_orderBy = column;
return this;
}
}
3. Factory Methods
// Acceptable: Creation returns the created object
public static Order Create(int customerId, IEnumerable<OrderItem> items)
{
return new Order
{
Id = Guid.NewGuid(),
CustomerId = customerId,
Items = items.ToList()
};
}
CQRS - Command Query Responsibility Segregation
CQRS applies CQS at the architectural level, using separate models for reads and writes.
┌─────────────────────────────────────┐
│ APPLICATION │
└─────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ COMMANDS │ │ QUERIES │
│ (Write) │ │ (Read) │
└───────────────┘ └───────────────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Command │ │ Query │
│ Handlers │ │ Handlers │
└───────────────┘ └───────────────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Domain Model │ │ Read Model │
│ (Rich) │ │ (DTOs) │
└───────────────┘ └───────────────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Write DB │──────────────▶│ Read DB │
│ (Normalized) │ Sync/Events │ (Optimized) │
└───────────────┘ └───────────────┘
Commands and Queries
// === MARKER INTERFACES ===
public interface ICommand { }
public interface ICommand<TResult> { }
public interface IQuery<TResult> { }
// === COMMANDS ===
public record CreateOrderCommand(
int CustomerId,
List<OrderItemDto> Items
) : ICommand<Guid>;
public record UpdateOrderStatusCommand(
Guid OrderId,
OrderStatus NewStatus
) : ICommand;
public record CancelOrderCommand(Guid OrderId) : ICommand;
// === QUERIES ===
public record GetOrderByIdQuery(Guid OrderId) : IQuery<OrderDto?>;
public record GetOrdersByCustomerQuery(
int CustomerId,
int Page = 1,
int PageSize = 10
) : IQuery<PagedResult<OrderSummaryDto>>;
public record GetOrderStatisticsQuery(
DateTime From,
DateTime To
) : IQuery<OrderStatisticsDto>;
Command Handlers
// === HANDLER INTERFACES ===
public interface ICommandHandler<in TCommand> where TCommand : ICommand
{
Task HandleAsync(TCommand command, CancellationToken ct = default);
}
public interface ICommandHandler<in TCommand, TResult> where TCommand : ICommand<TResult>
{
Task<TResult> HandleAsync(TCommand command, CancellationToken ct = default);
}
// === IMPLEMENTATION ===
public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand, Guid>
{
private readonly IOrderRepository _orderRepository;
private readonly ICustomerRepository _customerRepository;
private readonly IEventPublisher _eventPublisher;
public CreateOrderCommandHandler(
IOrderRepository orderRepository,
ICustomerRepository customerRepository,
IEventPublisher eventPublisher)
{
_orderRepository = orderRepository;
_customerRepository = customerRepository;
_eventPublisher = eventPublisher;
}
public async Task<Guid> HandleAsync(CreateOrderCommand command, CancellationToken ct)
{
// Validate
var customer = await _customerRepository.GetByIdAsync(command.CustomerId, ct);
if (customer == null)
throw new CustomerNotFoundException(command.CustomerId);
// Create domain entity
var order = Order.Create(command.CustomerId);
foreach (var item