The idea
What it is
Command–Query Separation (CQS) is a tiny rule with a big payoff. Coined by Bertrand Meyer, it says: every method should be one of two things and never both. A command does something — it changes the object's state and returns nothing. A query answers something — it returns a value and changes nothing. Keep them apart and your code becomes far easier to reason about.
Think of a bar. You can ask the bartender a question — "how full is my glass?" — and you can give the bartender an order — "pour me a beer." Asking the question must not change anything; you can ask it ten times and the answer only changes when something else happens. Giving the order does change the world: now there's beer in your glass. A query is a question. A command is an order. CQS just says: don't build a method that secretly does both — a "question" that also pours a beer every time you ask it.
The one sentence to remember
Asking a question should not change the answer. A query returns a value and leaves the object untouched; a command changes the object and returns nothing. Never mix the two in one method.
CQS is not CQRS
These names look almost identical and get confused constantly. CQS is a method-level habit: a single method is either a command or a query. CQRS (Command Query Responsibility Segregation) is a system-level architecture: you split your whole application into a separate write model and read model, often with different databases and data paths. CQS is something you apply to one method in seconds; CQRS is a major design decision for a whole service. CQRS was inspired by CQS, but they operate at completely different scales — don't treat them as the same thing.
Mechanics
How it works
Two kinds of method, and only two
Sort every method you write into one of these two boxes. Most methods fall in cleanly, and the type signature usually gives it away.
- Command — it does something. It changes the object's state (adds an item, transfers money, clears a list) and returns nothing (
void). Examples:deposit(amount),addItem(item),clear(). - Query — it answers something. It returns a value and changes nothing. You can call it a hundred times in a row and get the same answer (assuming nothing else changed). Examples:
getBalance(),isEmpty(),totalItems().
A quick litmus test
Look at the return type and the body. Returns `void` and touches state? That's a command. Returns a value and only reads state? That's a query. If a method both returns a value and mutates state, a CQS alarm should go off — you've probably built something that surprises its callers.
The danger: a query that secretly mutates
The whole reason CQS exists is to stamp out one specific bug: a method that looks like a query — it returns a value, so callers assume it's safe to read — but secretly changes state every time it runs. The two classic offenders:
stack.pop(); // returns the top item AND removes it ⚠️ both!
idGen.getNextId(); // returns an id AND increments the counter ⚠️ both!Both look like questions — they hand you back a value — but each one also mutates. That breaks the most useful property of a query: safe repeatability. If getNextId() were a pure query you could call it twice and get the same id. Because it secretly increments, calling it twice gives you 1 then 2. The caller who just wanted to peek at "the next id" has now silently burned an id. A method named like a question that behaves like an order is a landmine.
Why "call it twice" is the killer test
A pure query can be called as many times as you like, in any order, with no consequence — that's what makes reading state safe. The moment a query has a hidden side effect, you can no longer call it freely: a debugger that evaluates it, a log line that prints it, or a retry that repeats it will each change your program's state. The surprise is invisible at the call site, so the bug hides for a long time. If calling a method twice changes the answer, it should never have looked like a query.
The fix: split the trap into two honest methods
When you find a method that both answers and acts, split it in two — a pure query that only reads, and a command that only acts. Instead of one popLast() that returns and removes, expose a peekLast() query (read the last item, change nothing) and a removeLast() command (drop the last item, return nothing). Callers compose them when they truly need both:
// BEFORE — one method that both answers and mutates (violates CQS)
const item = cart.popLast(); // returns AND removes
// AFTER — a query and a command, each honest about what it does
const item = cart.peekLast(); // query: read it, cart unchanged
cart.removeLast(); // command: remove it, returns nothingNow peekLast() is safe to call in a log line, a debugger, or a retry — it never changes the cart. removeLast() is clearly an action. The caller who wants the old behavior simply calls both, on purpose, in full view. Nothing happens by surprise.
popLast() does two jobs at once — it answers (returns the item) and acts (removes it). A caller who only wanted to read can't, because reading mutates. Right: split it. peekLast() is a pure query (read, no change) and removeLast() is a pure command (mutate, return nothing). Now you read safely as often as you like, and act only when you mean to.The famous pragmatic exceptions
CQS is a guideline, not a law of physics, and a few well-known methods break it on purpose because the combined operation is genuinely useful and the side effect is expected. stack.pop(), queue.poll(), and iterator.next() all return a value and advance/mutate — and that's fine, because everyone already knows they do, and "take the next one" is one atomic idea. The rule of thumb: it's acceptable to combine when the mutation is the whole point of the call and is obvious from the name. The trap is the accidental mix — a getX() or isReady() that nobody expects to change anything but quietly does.
Naming carries the contract
Half of CQS is in the names. get…, is…, has…, count…, peek… promise a query — callers will read them freely, so they had better be side-effect-free. add…, remove…, set…, clear…, save… promise a command. When a name promises a question but the body gives an order, you've broken the contract the caller is relying on — even if the compiler never complains.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
Sort each method of a ShoppingCart into one of two buckets — COMMAND (does something, returns nothing) or QUERY (answers something, no side effects). Click a card; the single explain panel tells you instantly whether it's a command or a query and why. Then a red TRAP card appears — popLast(), which returns the last item and removes it — and it fits neither bucket cleanly. The panel flags the CQS violation and shows the compliant split (peekLast() query + removeLast() command). A tiny call it twice demo proves the point: a pure query gives the same answer both times (safe); the trap changes the cart the second time. One fixed panel, replaced each click; nothing scrolls away.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
Sort the obvious ones
Open the prototype. You'll see method cards from a ShoppingCart — getBalance(), addItem(x), isEmpty(), clear(), totalItems(), deposit(x). Click each card and drop it into COMMAND or QUERY. Use the litmus test: returns a value and only reads → query; returns void and changes state → command. The single explain panel tells you instantly whether you were right and why, and a running tally counts how many you've sorted correctly.
Meet the trap card
After the clean cards are sorted, a red TRAP card appears: popLast(). Try to drop it in a bucket. The panel flags it: it returns the last item (looks like a query) and removes it (acts like a command), so it fits neither cleanly — it violates CQS. Read the suggested fix: split it into peekLast() (a pure query) and removeLast() (a pure command).
Call it twice
Run the call it twice demo. First call a pure query — say totalItems() — twice in a row: the cart is unchanged, so you get the same answer both times (safe to repeat). Then call the trap popLast() twice: the first call removes the last item, and the second call removes a different one — the answer changed because asking the question mutated the cart. That single demo is the whole reason CQS exists: a query you can't safely repeat isn't really a query.
In practice
When to use it — and what trips people up
Apply CQS to almost every method you write
Unlike heavier patterns, CQS is cheap enough to be a default habit. As you write each method, ask: is this an order or a question? Make it clearly one or the other. It pays off most in the places where a hidden side effect would hurt:
- Anything named like a question —
get…,is…,has…,count…. Callers, debuggers, and log lines will read these freely, so they must be side-effect-free. - Code that gets retried or logged — if a value-returning method might run twice (a retry, a
console.log, a debugger watch), it must be safe to repeat. CQS guarantees that for queries. - Domain models and value objects — keeping reads pure makes objects predictable and easy to test: call a query, assert the result, with no setup teardown for surprise mutations.
- APIs other people call — a public method's name is a promise. A query that mutates breaks that promise invisibly, and the bug lands in their code, not yours.
Know the sanctioned exceptions
Don't fight idioms that already combine the two by well-known convention: stack.pop(), queue.poll(), iterator.next(), an atomic getAndIncrement(), or compareAndSet(). These return and mutate on purpose, everyone expects it, and the combined step is genuinely atomic. CQS targets the accidental mix — the getStatus() that quietly logs, increments, or lazily mutates. Combine deliberately and obviously, or not at all.
What it gives you
- Safe to read — pure queries can be called any number of times, in any order, by code, logs, or a debugger, with zero risk of changing program state.
- Easier to reason about — a method's name and return type tell you whether it acts or answers, so you can predict behavior without reading the body.
- Simpler testing — query results are deterministic and repeatable; you assert on them without worrying that the act of checking changed the object.
- Catches a whole bug class — the 'looks like a query but mutates' landmine simply can't exist if you follow the rule.
Common mistakes
- Occasional extra call — when you genuinely need a value and a mutation, you call two methods (peek then remove) instead of one combined pop.
- Famous exceptions exist — pop/poll/next/getAndIncrement deliberately break CQS, so it's a strong guideline, not an absolute law.
- Not always atomic — splitting a combined operation into query+command can introduce a race in concurrent code, where the single atomic op was actually safer.
- Easy to confuse with CQRS — the near-identical name leads people to over-engineer a whole architecture when they only needed the method-level habit.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
class ShoppingCart {
private items: string[] = [];
// COMMANDS — do something, change state, return nothing (void).
addItem(item: string): void { this.items.push(item); }
clear(): void { this.items = []; }
// QUERIES — answer something, return a value, change nothing.
totalItems(): number { return this.items.length; }
isEmpty(): boolean { return this.items.length === 0; }
peekLast(): string | undefined { return this.items[this.items.length - 1]; }
// ⚠️ VIOLATION — returns the item AND removes it: both query and command.
popLast(): string | undefined {
return this.items.pop(); // looks like a read, but mutates every call
}
// ✅ COMPLIANT — split the trap into a pure command (peekLast is the query above).
removeLast(): void { this.items.pop(); }
}
const cart = new ShoppingCart();
cart.addItem("milk"); // command
cart.addItem("bread"); // command
// A pure query is safe to repeat — same answer twice, cart untouched.
cart.totalItems(); // 2
cart.totalItems(); // 2 (no surprise)
// Need the value AND the removal? Compose the two honest methods, on purpose.
const last = cart.peekLast(); // query: "bread", cart unchanged
cart.removeLast(); // command: removes it, returns nothingReferences & further reading
6 sources- Articlemartinfowler.com
CommandQuerySeparation — Martin Fowler (bliki)
Fowler's crisp explanation of the principle, where it helps, and the pragmatic exceptions (like pop) where mutating-and-returning is acceptable. The best short read on the topic.
- Docsen.wikipedia.org
Command–query separation — Wikipedia
Concise reference covering the formal definition, Meyer's origin, the connection to referential transparency, and how CQS relates to (but differs from) CQRS.
- Booken.wikipedia.org
Object-Oriented Software Construction — Bertrand Meyer
The book where Meyer introduced CQS as part of the Eiffel design philosophy. The primary source for the principle and the reasoning behind 'asking a question shouldn't change the answer'.
- Articlemartinfowler.com
CQRS — Martin Fowler (bliki)
Read this to keep CQS and CQRS straight: Fowler explains that CQRS splits an entire system into separate read and write models, a far bigger commitment than the method-level CQS habit.
- Articlegregyoung.wordpress.com
CQS versus CQRS — Greg Young
From the person who popularized CQRS: a clear statement that CQRS is an architectural pattern at the object/service level, distinct from Meyer's method-level CQS. Settles the 'are they the same?' question.
- Talkyoutube.com
Command Query Separation explained (video)
A short, visual walkthrough of CQS with live code: spotting a method that both returns and mutates, then splitting it into a clean query and command.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Sign in to save your best score.
question 01 / 05
What does Command–Query Separation actually require?
question 02 / 05
Which method is the clearest CQS violation?
question 03 / 05
Why is a query with a hidden side effect so dangerous?
question 04 / 05
How do CQS and CQRS differ?
question 05 / 05
Methods like stack.pop() and iterator.next() return a value AND mutate. How does CQS treat them?
0/5 answered