In Domain-Driven Design's Supple Design, what is the practical difference between a 'command' and a 'side-effect-free function' (query), and why is it worth deliberately separating them even though a single method could do both more 'efficiently'?
answer
- CQS: command changes, query answers
- queries are safe to call twice
- pop() problem: mutate+return in one call
- push mutation into a thin, explicit command layer
- complex business rules as pure functions
basics
~20 sA query just answers a question and changes nothing; a command changes something and doesn't need to return an answer. Keeping them separate means you can call a query as many times as you like without worrying it will break anything.
solid answer
~40 sCommand-Query Separation (CQS), which Evans folds into Supple Design as 'side-effect-free functions,' says operations should either return a value and change nothing (functions) or change state and return little or nothing (commands), but not both. Side-effect-free functions are safe to compose, cache, retry, and call in any order, since you don't need to trace the whole call graph to know if calling them twice is dangerous. The trade-off is that some operations naturally want to both mutate and report (e.g. a stack pop()), and splitting them into top() plus removeTop() can introduce a race between the two calls in concurrent code. So it's applied pragmatically: complex domain logic and business rules get pushed into pure functions wherever possible, while an explicit, clearly-named set of commands handles the necessary mutations.
go deeper
Can state the basic rule: a method should either answer a question or change something, not secretly both, and can point to an obvious violation like a getter that also saves to the database.
Applies CQS when writing new domain methods, separates a combined read+mutate operation into a query plus a command when there's no concurrency concern, and writes simple unit tests for the pure functions without mocking state.
Knows the deliberate exceptions (pop/dequeue, check-and-set) where splitting the operation introduces a race, and decides case by case, naming the exception clearly as a command.
Sets the architectural default of pushing business rules into a pure-function core with a thin, explicit command/mutation shell, and can justify the testing and concurrency-safety payoff to a team resisting the extra methods as 'boilerplate.'
## The two kinds of operation **Command-Query Separation (CQS)**, which Evans incorporates into Supple Design as 'side-effect-free functions,' splits every operation into one of two kinds. | Kind | What it promises | |---|---| | **A query (function)** | returns a value, answers a question about the current state, and changes nothing observable -- you should be able to call it zero, one, or a hundred times in a row and get the same answer with no other effect on the system | | **A command** | changes state -- writes a field, persists a row, sends a message -- and, ideally, returns little or nothing, so nobody is tempted to treat its return value as a safe-to-repeat answer | Applying this concretely means auditing a method's body: if it both mutates an object or database and returns a computed value derived from that mutation, it's mixing the two roles, and the fix is either to split it into two named calls or to make peace with it being an explicit command whose name signals mutation clearly. ## Why the separation pays This separation exists because side-effect-free functions are dramatically easier to reason about, test, and compose. - A pure function's correctness can be checked with input/output examples alone -- no database fixture, no mock, no ordering dependency between test cases -- and it can be called from anywhere (a UI preview, a batch job, a retry loop) without worrying whether a second call will corrupt state. - In a domain model, most of the genuinely hard logic (pricing rules, eligibility checks, date math) is naturally computational rather than state-changing, so pushing that logic into a layer of pure functions and confining the necessarily state-changing operations to a small, explicit set of commands keeps the bulk of the domain model safe to refactor and safe to reuse in contexts the original author never anticipated. ## What the split costs The cost shows up in two places. 1. **First**, splitting an operation that's naturally 'read and change' (like a stack `pop()` or `dequeue()`) into two calls (`top()` then `removeTop()`) can genuinely be worse: under concurrency, another thread can mutate the collection between the two calls, so what you read is no longer guaranteed to be what you removed -- the atomic combined operation existed for a correctness reason, not laziness. 2. **Second**, going fully pure everywhere is not free: real systems must persist orders, decrement inventory, and send emails, and if a team treats 'no side effects' as an absolute rule, mutation logic doesn't disappear, it just gets pushed into a poorly-organized corner or into an anemic domain model where all mutation lives in generic 'service' classes with no domain meaning at all. ## Failure modes - In practice, this shows up as a getter that quietly writes to a database (a `getCachedPrice()` that repopulates a stale cache as a side effect), which becomes a production incident when a monitoring dashboard that 'just reads' a metric ends up mutating state under load and racing with the real write path. - Another common failure is unit tests for a supposedly pure calculation method that need database mocks or `verify()` assertions on write calls -- a strong signal the method isn't actually a query, whatever its name claims. - A third failure is renaming a command to sound safe, e.g. calling a method that both validates and persists `validate()`, which misleads a caller into invoking it speculatively in a loop to check several candidate inputs and unintentionally persisting several of them. ## A checkout flow, both sides A checkout flow illustrates the split well: - `calculateOrderTotal(cart): Money` is a pure function -- given the same cart contents, it always returns the same total, with no database write, so a 'review your order' screen can call it as often as the user edits their cart without any risk. - `placeOrder(cart): OrderId` is the command -- it persists the order, decrements inventory, and triggers a confirmation email, and it is called exactly once, deliberately, when the user clicks 'buy.' Keeping these as two distinctly named operations means the pricing logic can be unit tested with trivial input/output assertions and reused in a discount-preview feature months later, while the actual state-changing commit path stays small, explicit, and easy to audit for exactly where mutation happens in the system.
- Is command-query separation always achievable, and what do you do when it isn't, e.g. a queue's pop operation?No -- some operations are inherently both, like a stack or queue pop that must atomically read-and-remove to avoid a race in concurrent code. In those cases you keep the combined operation but name it unambiguously as a command (pop(), dequeue(), not peek() or get()), and if pure read access is also needed you add a separate, genuinely side-effect-free peek()/top() alongside it.
- How does pushing business rules into side-effect-free functions make the domain model easier to change?Pure functions can be unit tested with plain input/output assertions, no mocks or fixtures for state, and they can be freely reordered, memoized, or reused in new contexts because they carry no hidden dependency on when or how many times they're called. That makes large swaths of the domain logic safe to refactor, since tests catch a broken pure function immediately, whereas a broken side effect can silently corrupt state and only surface later.
- What's the risk of over-applying this and making every domain method side-effect-free?At some point state has to change -- orders get placed, inventory gets decremented -- and forcing every operation to be pure just pushes the mutation into an ever-larger, less transparent set of ad hoc command methods or an anemic domain model where all the mutation logic leaks into services. The goal is a deliberate, small, well-named set of commands, not the elimination of state change.
Like the difference between asking a librarian 'how many books are on this shelf?' (harmless, ask as often as you like) and telling them 'remove a book from this shelf' (changes the world, so you'd better only say it once) -- mixing the two into 'count and maybe remove a book, I'm not telling you which' is confusing and dangerous.
saying these in an interview costs you the question
- a getter that also writes to the database or increments a counter
- boolean-returning method that also performs the mutation it's reporting on
- unit tests for a 'query' method that need to assert database side effects
- renaming a command to sound like a query, e.g. calling validateAndSave() just validate()
- claiming purity is always achievable and dismissing pop()-style cases