Beginner14 min readDesign Principles & Heuristicslive prototype

Command–Query Separation (CQS)

Bertrand Meyer's rule: every method should be either a command that *does* something (changes state, returns nothing) or a query that *answers* something (returns a value, changes nothing) — never both. Asking a question shouldn't change the answer, so you can read state as often as you like without surprises.

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:

typescript
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:

typescript
// 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 nothing

Now 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.

VIOLATION — does both COMPLIANT — split in two popLast() returns AND removes one method, two jobs answers ✦ acts — caller can't read safely peekLast() QUERY · reads, no change removeLast() COMMAND · mutates, returns void each honest — read safely, act on purpose
Left: one 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.

try 01

Sort the obvious ones

Open the prototype. You'll see method cards from a ShoppingCartgetBalance(), 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.

try 02

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).

try 03

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 questionget…, 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 nothing

References & further reading

6 sources

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