midarchitecture
Command Query Responsibility Segregation — separate models for reading (Query) and writing (Command). Different optimizations for each.
Query: read model, optimized for SELECT (denormalized, projections, read replicas). Command: write model, optimized for business logic and validation. Simple CQRS: different DTOs/services. Advanced: separate databases (PostgreSQL for write, Elasticsearch for read). Event Sourcing is often combined with CQRS but is not required.
// Command
public record CreateOrderCommand(int UserId, List<OrderItem> Items) : IRequest<int>;
public class CreateOrderHandler(AppDbContext db) : IRequestHandler<CreateOrderCommand, int>
{
public async Task<int> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
var order = new Order(cmd.UserId, cmd.Items);
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return order.Id;
}
}
// Query — can use a separate read DB or Dapper
public record GetOrderQuery(int Id) : IRequest<OrderDto?>;
public class GetOrderHandler(IDbConnection conn) : IRequestHandler<GetOrderQuery, OrderDto?>
{
public async Task<OrderDto?> Handle(GetOrderQuery q, CancellationToken ct)
=> await conn.QuerySingleOrDefaultAsync<OrderDto>(
"SELECT id, total, status FROM orders WHERE id = @Id", q);
}When yes
Read/write ratio differs significantly, complex domain logic, different scaling requirements
When no
Simple CRUD — the overhead is not justified. Small team — too much complexity. Without Event Sourcing, CQRS is simpler
Interview tip
CQRS without Event Sourcing is perfectly normal and common. Don't overcomplicate things if you don't need event history.