CQS — Command Query Separation
Command Query Separation is a simple, method-level rule: a method should either change state (a command, returning nothing) or return a value (a query, changing nothing) — never both. Asking a question should not alter the answer. It is small, cheap, and applies almost everywhere, and it is the principle that CQRS scales up to the architecture level.
CQS, coined by Bertrand Meyer, draws a line between two kinds of methods. A query computes and returns a result and has no observable side effects — you can call it twice and nothing changes. A command performs an action that changes state and returns nothing (or just an acknowledgement). The trouble comes from methods that do both: they return data and mutate, so reading becomes risky and reordering or removing a call has surprising effects.
CQS is a coding discipline, not an architecture — keep it distinct from CQRS, which applies the same idea at the system level with separate read and write models. CQS costs almost nothing to follow and makes code dramatically easier to reason about, so it is a near-universal default; it reinforces Separation of Concerns and Mutable vs Immutable Design.
Keep questions and actions apart
- DoMake queries pure: they return a value and have no observable side effect. Calling a query any number of times, or not at all, must not change behaviour.
- DoMake commands change state and return `void`/`Task` or a minimal result (an id, a success flag) — not a rich payload that tempts callers to treat the command as a read.
- DoName methods so the kind is obvious: `Get`, `Find`, `Calculate` for queries; `Apply`, `Submit`, `Record`, `Send` for commands.
- ConsiderA small, well-known exception (like a stack `Pop` or `GetOrAdd`) only when it is idiomatic and the dual nature is unmistakable. Even then, prefer a clearer split if you can.
- NeverMutating state inside a getter, a property, or anything that reads like a question. A property access that secretly changes things is a debugging nightmare.
public Customer GetCustomer(Guid id) {
var c = repo.Load(id);
c.LastAccessedAt = DateTime.UtcNow; // hidden write
repo.Save(c);
return c;
}
"Get" implies a harmless read, but every call writes to the database. Logging or a debugger watch that calls it changes production state.
public Customer GetCustomer(Guid id) => repo.Load(id); // pure read
public Task RecordAccess(Guid id, DateTimeOffset at) // explicit write
=> repo.UpdateLastAccessed(id, at);
The read is safe to call freely; the write is an explicit, named action. Reordering or removing the read can no longer have side effects.
Why it pays off
- DoReason about and test queries in isolation — no setup of side effects, no teardown, safe to call in assertions and logs.
- DoCache, retry, and parallelise queries safely, because they don't change anything. The same freedom you get from immutable values, at the method level.
- ConsiderCQS as the on-ramp to CQRS: code already written as clean commands and queries is far easier to split into separate models later if scaling demands it.
Self-review checklist
- AskDoes this method both return data and change state? If so, can I split it?
- AskCould I call this query twice with no effect — and does its name promise that?
- AskIs anything mutating inside a getter or property?