Select a result to preview
Command-Query Separation (CQS) is a design principle that says every method should be either a command (changes state, returns void) or a query (returns data, has no side effects) — never both. Coined by Bertrand Meyer, it makes code easier to reason about: if a method returns a value, you can call it freely without worrying about side effects; if it changes state, you know it won't return data you depend on. In a 200-endpoint e-commerce API, applying CQS to the repository layer made it immediately clear which methods could be safely retried, cached, or called in parallel (queries) and which required idempotency guards and transaction boundaries (commands) — cutting the time to diagnose a double-charge bug from hours of tracing to minutes of checking command call sites.
CQS is a method-level principle. CQRS (Command-Query Responsibility Segregation) applies the same idea at the architectural level — separate read and write models, separate data stores, separate code paths.
// 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 places the order, then queries for it separately if needed. This is slightly more verbose but makes each method's contract explicit.
Strict CQS is sometimes impractical. Common exceptions:
Peek() + Remove() introduces a race condition in concurrent code.Task<T> methods that perform I/O and return a result are idiomatic in .NET even when they have side effects.The principle is a guideline, not a law. Apply it where it improves clarity; relax it where strict adherence creates awkward APIs.
| CQS | CQRS | |
|---|---|---|
| Scope | Method level | Architecture level |
| Separation | Commands and queries in the same class | Separate command and query models/handlers |
| Data store | Single shared store | Often separate read/write stores |
| Complexity | Low | High |
CQS is a prerequisite mindset for CQRS. If you're applying CQRS, you're already following CQS at the method level. See CQRS for the architectural pattern.
A CQS-compliant repository separates read and write methods with explicit contracts:
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 query methods can be called freely in any order without side effects. The command methods are the only paths that change state — making it easy to audit what can mutate the system.
What goes wrong: repository.Add(entity) returns the saved entity with its generated ID. This is a command (changes state) that also returns data — a CQS violation. In one codebase, a CreateOrderAsync method that returned the full Order entity was called by two API consumers: one retried on timeout, creating duplicate orders because the caller treated the returned entity as idempotent confirmation. Separating into a void command + separate query would have made the retry-safety question obvious.
Why it matters: the violation is pragmatic and widely accepted (see 'When CQS Is Pragmatically Relaxed' above), but it should be a conscious decision. Undisciplined mixing of commands and queries makes code harder to reason about and test.
Mitigation: document the exception explicitly. For new code, prefer returning only the generated ID from commands (not the full entity), then let the caller query if they need the full state.
When a method returns a value, you know it has no side effects — you can call it freely, cache its result, or call it multiple times without changing state. When a method returns void, you know it changes state but produces no data you depend on. This separation eliminates the need to read the implementation to understand whether a call is safe to repeat or reorder. It also makes testing easier: queries can be tested without checking state changes; commands can be tested without checking return values.