Command-Query Separation (CQS) divides operations by contract. A command changes state and traditionally returns no result. A query returns data without observable side effects. Bertrand Meyer introduced the rule so a call’s shape reveals whether it can mutate the system.

CQS applies to individual operations. Software Architecture/Patterns/Architectural Patterns/CQRS uses a related split at system level, where command and query paths may have different models. Separate databases are optional.

The Principle in Practice

// VIOLATES CQS: changes state AND returns data
public Order PlaceOrder(Cart cart)
{
    var order = new Order(cart);
    _db.Orders.Add(order);
    _db.SaveChanges();
    return order;  // side effect + return value
}
 
// CQS-compliant: separate command and query
public void PlaceOrder(Cart cart)          // command: changes state, returns void
{
    var order = new Order(cart);
    _db.Orders.Add(order);
    _db.SaveChanges();
}
 
public Order GetOrder(OrderId id)          // query: returns data, no side effects
    => _db.Orders.Find(id) ?? throw new NotFoundException(id);

The caller issues the state change and performs a separate read only when current state is needed. The extra call is worthwhile when it makes mutation and retry behavior easier to see.

When CQS Is Pragmatically Relaxed

Strict CQS sometimes produces a worse contract:

  • Stack.Pop() expresses removal and return as one coherent operation. Concurrent code needs a thread-safe contract such as ConcurrentStack<T>.TryPop() to make that combined transition atomic.
  • A create operation may return a database-generated identifier because a second round trip adds no clarity.
  • Many .NET APIs use Task<T> for an operation that performs I/O and reports its outcome. The return type alone no longer proves purity.

Keep queries safe to observe and make command mutation and retry behavior explicit. Do not split an atomic operation merely to satisfy the surface rule.

CQS Vs CQRS

CQSCQRS
ScopeMethod levelArchitecture level
SeparationCommands and queries in the same classSeparate command and query models/handlers
Data storeSingle shared storeOften separate read/write stores
ComplexityLowHigh

Software Architecture/Patterns/Architectural Patterns/CQRS applies the command/query distinction to separate write and read models, but it does not prove that every method follows strict CQS.

CQS in a Repository

A repository can expose the distinction directly:

public interface IOrderRepository
{
    // Queries: return data, no side effects
    Task<Order?> GetByIdAsync(OrderId id);
    Task<IReadOnlyList<Order>> GetByCustomerAsync(CustomerId customerId);
 
    // Commands: change state, return void (or Task)
    Task AddAsync(Order order);
    Task UpdateAsync(Order order);
    Task DeleteAsync(OrderId id);
}
 
// The generated ID exception: returning the ID from Add is a pragmatic CQS violation.
// Document it explicitly:
// Task<OrderId> AddAsync(Order order);  // returns generated ID only, not the full entity

The interface makes mutation paths visible. Query implementations must still avoid hidden writes such as updating last-accessed timestamps. A method name alone cannot guarantee the contract.

Pitfalls

Violating CQS in Repository Methods

When repository.Add(entity) returns the saved entity, mutation and observation share one contract. That exception may be reasonable, but the returned object does not prove that retrying the command is safe.

Return only the outcome the caller needs, often an identifier or version, and state retry behavior separately. Fetch the full current representation through a query.

Questions

References