Skip to main content
CleanCodeMastery

Separate Query from Modifier: Asking Your Bill Must Not Add Chai to It

Separate Query from Modifier explained simply — split a method that both answers a question and changes state into a pure query and a separate command, following Bertrand Meyer's Command-Query Separation principle: asking a question should not change the answer.

25 min read Updated June 11, 2026intermediate
refactoringseparate query from modifiercommand query separationCQSside effectspure functionssimplifying method calls

☕ "Bhaiya, How Much Do I Owe?" — And the Bill Grows

There is a tea stall near the bus stop run by Gopal bhaiya, where regulars keep a monthly khata — a running account in a fat, tea-stained notebook. Ramesh, who works at the bank opposite, drinks chai there every day and settles his page on salary day. The system runs on pure trust, like a thousand tea stalls across the country.

Now imagine this. Mid-month, Ramesh asks, "Bhaiya, how much do I owe so far?" Gopal bhaiya flips to Ramesh's page, smiles, and says "Two hundred and ten rupees" — and while answering, quietly writes today's chai onto the page, even though Ramesh has not ordered anything today.

Ramesh asked a question. Gopal bhaiya performed an action. Ramesh has no idea. Next week he asks again, just to keep track — "Two hundred and thirty now, Ramesh babu." Thirty rupees, though he visited only once in between! Every time he checks his bill, his bill grows. Slowly the regulars notice. Sunita from the tailor shop stops asking her total — "asking itself costs money at this stall," she warns the others. The notebook can no longer be trusted, and neither can the shopkeeper's answers. And here is the saddest part: an honest stall runs on people checking freely — that is what keeps trust alive. The moment questions become dangerous, everyone stops asking, and small mistakes hide in the notebook until the big salary-day argument.

The rule of every honest shop is simple: asking the price must never change the price. Questions are free, safe, and repeatable. Actions — ordering a chai, paying the bill — change things, and you do them on purpose, knowingly, out loud.

Code deserves the same honesty. A method that answers a question should never also change things behind the caller's back. When one method does both — returns the balance and charges a fee, checks a password and locks the account — every caller who "just wanted to know" silently triggers changes. Separate Query from Modifier is the refactoring that splits such a method into two honest halves: a pure question and a deliberate action.

Figure 1: The khata's journey — from trusted notebook, through sneaky entries, to honesty restored by separating asking from charging.

What is Separate Query from Modifier? 🧐

Separate Query from Modifier is a refactoring from Martin Fowler's Refactoring book:

Split a method that both returns a value and changes state into two methods — a pure query that returns the value with no side effect, and a command (modifier) that performs the change.

Behind this refactoring stands one of the oldest and most useful principles in object-oriented design: Command-Query Separation (CQS), formulated by Bertrand Meyer — the creator of the Eiffel programming language — in his 1988 book Object-Oriented Software Construction. Meyer's rule divides all methods into exactly two kinds:

  • A query answers a question. It returns data and changes nothing observable. Call it once, call it fifty times, call it in any order — the world stays the same.
  • A command performs an action. It changes state — and returns nothing (so nobody is tempted to use it as a question).

The unforgettable one-liner: asking a question should not change the answer.

Why is this worth a principle of its own? Because side-effect-free queries are a superpower. If you know a method changes nothing, you can:

  • call it anywhere — in an if condition, a log line, an assertion, a debugger watch window — with zero fear;
  • call it any number of times — twice, in a loop, from two places — and get consistent answers;
  • cache or reorder it freely, since nothing depends on when or how often it runs;
  • test it with plain input-output checks, no state verification gymnastics.

The moment a query hides a side effect, every one of those guarantees silently dies — and the bugs that follow are the nastiest kind, because they appear where someone "just read a value." A teammate adds an innocent log line, logger.info(account.getBalance()), and customers start getting double-charged fees. Nobody suspects the log line for days.

💡

Where to read the canon: this refactoring appears as Separate Query from Modifier in both editions of Fowler's Refactoring (unlike Rename Method / Add Parameter / Remove Parameter, it was not merged into Change Function Declaration — it kept its own name in the 2nd edition's catalog). The principle behind it, CQS, has its own page on Martin Fowler's bliki, where he credits Bertrand Meyer and also honestly lists the classic exceptions like stack.pop(). Refactoring and principle are a pair: CQS is the rule; Separate Query from Modifier is how you repair code that breaks the rule.

A naming bonus: once you split the method, the two halves finally get honest names — the query a noun-ish question (pendingBalance()), the command a verb (applyLateFee()). If you ever find a method whose only truthful name needs an "And" in the middle — getBalanceAndApplyFee — CQS has already been violated, and this refactoring (plus Rename Method) is the cure.

The whole principle on one page:

Figure 2: CQS in one picture — queries answer, commands act, and the reason is that questions must stay free and safe.

College corner: the web is built on this exact idea. HTTP defines GET as a safe method — it must not change server state — and idempotent: sending it once or fifty times yields the same effect. POST, PUT, and DELETE are the commands. Every cache, proxy, prefetcher, and crawler on the internet relies on GET being a pure query: browsers happily prefetch links, CDNs serve cached GET responses without asking the server, and monitoring bots poll endpoints all day. A backend that mutates state inside a GET handler — say, GET /api/balance charging a fee — breaks the entire contract: one crawler visit, one prefetch, one retry by a flaky network, and state changes nobody asked for. CQS at method level and "GET must be safe" at protocol level are the same law at two scales.

When Do We Need It? 🚨

Use this refactoring when you see these signs — each is the shopkeeper writing in the notebook while answering you:

  1. A "get" that also sets. getNextInvoiceNumber() returns the number and increments the counter. Display the number twice on screen, and you have skipped an invoice number forever.
  2. A check that also acts. isPasswordValid(pw) returns false and increments the failed-attempts counter and maybe locks the account. Now a unit test that calls it three times locks the test user. An admin tool that "just checks" locks real customers.
  3. A query unsafe to repeat. Code comments like // do not call twice! above a method that looks like a read. The comment is a confession.
  4. Strange caller rituals. Callers save the result in a variable and pass it around for fear of calling again; or they call the method and discard the result (using it as a pure command) — proof that two different needs are trapped inside one method.
  5. Assertions and logs cause bugs. Adding an assert or a debug log changes program behaviour. Meyer designed CQS hand-in-hand with Design by Contract precisely so assertions — which evaluate queries — could never alter the program.
  6. The "And" name. As above: if the honest name has "And" in it, the method has two jobs. (Related smell: methods doing two jobs are often hiding inside a Long Method.)

Signal number 4 is worth a chart. When you audit the callers of a typical combined method, you find most of them never wanted both halves at all:

Figure 3: Audit the callers of a combined method and most only wanted the answer — yet every one of them triggered the action.

Sixty percent of callers are Ramesh: they asked a question and got billed for it.

And the honest exceptions, where query and command must stay together:

  • Atomic operations under concurrency: stack.pop(), queue.dequeue(), iterator.next(), compare-and-swap, "fetch next ID". Splitting these into peek-then-remove invites another thread between the two calls — a race condition. Fowler explicitly accepts these deviations.
  • Expensive shared computation: if the command must compute the value anyway at real cost, recomputing it in a separate query may be wasteful — measure before splitting.
  • Transaction boundaries: two calls may observe different states than one atomic call would; keep atomicity where correctness depends on it.

Two questions decide whether to split or stay atomic — how contested is the state, and how entangled is the computation:

Figure 4: Split eagerly when state is single-threaded and the question is cheap; respect atomicity when many writers race for the same state.

Before and After at a Glance 📋

The tea-stall notebook, in TypeScript:

// BEFORE — asking the bill ADDS to the bill
class KhataAccount {
  private entries: number[] = [];
 
  // Query and command glued together — the dishonest shopkeeper
  getBalanceAndChargeServiceFee(): number {
    const balance = this.entries.reduce((sum, e) => sum + e, 0);
    if (balance > 200) {
      this.entries.push(10); // sneaky Rs.10 "service fee" — a SIDE EFFECT
    }
    return balance;
  }
}
 
// An innocent caller, just displaying the khata:
const due = account.getBalanceAndChargeServiceFee(); // charged!
showOnScreen(due);
console.log(account.getBalanceAndChargeServiceFee()); // charged AGAIN, and shows a different number!

Two reads, two surprise fees, two different answers to the same question. Now the honest split:

// AFTER — a pure question and a deliberate action
class KhataAccount {
  private entries: number[] = [];
 
  // QUERY — safe to call any number of times, anywhere
  getBalance(): number {
    return this.entries.reduce((sum, e) => sum + e, 0);
  }
 
  // COMMAND — changes state, returns nothing, called on purpose
  chargeServiceFeeIfDue(): void {
    if (this.getBalance() > 200) {
      this.entries.push(10);
    }
  }
}
 
// Callers now choose explicitly:
showOnScreen(account.getBalance());        // ask freely — nothing changes
console.log(account.getBalance());         // same answer, guaranteed
 
account.chargeServiceFeeIfDue();           // the action happens HERE, visibly, on purpose

Same total behaviour available — but now reading is free, and charging is a visible, deliberate line of code that reviewers can see and question.

The two-faced exchange, frame by frame — watch the hidden write happen while the caller thinks they are only reading:

Figure 5: Before the split, every innocent question secretly performs an action — the caller never sees the write happen.

After the split, the class declares its honesty in its very shape — return types tell the story: queries return values, commands return void.

Figure 6: An honest CQS class — you can classify every method as query or command from the signature alone.

Step-by-Step, the Safe Way 🪜

We will split getBalanceAndChargeServiceFee() carefully, keeping the program green at every step.

Figure 7: The split pipeline — the combined method survives as glue until every caller has declared its true intent.

Step 1 — Create the pure query. Copy only the value-producing logic into a new method. Triple-check it touches nothing: no assignments to fields, no saves, no sends.

// Step 1 — the new pure query (original method still exists, untouched)
getBalance(): number {
  return this.entries.reduce((sum, e) => sum + e, 0);
}

Step 2 — Make the original method use the query for its return value. Behaviour identical; we are only de-duplicating inside.

// Step 2 — original now delegates the "question" part
getBalanceAndChargeServiceFee(): number {
  const balance = this.getBalance();
  if (balance > 200) {
    this.entries.push(10);
  }
  return balance;
}

Compile, run tests. Everything must pass — nothing observable changed.

Step 3 — Carve out the command. Move the side-effecting part into a verb-named, void-returning method, and let the original call both halves:

// Step 3 — the command exists; original is now just a glue method
chargeServiceFeeIfDue(): void {
  if (this.getBalance() > 200) {
    this.entries.push(10);
  }
}
 
getBalanceAndChargeServiceFee(): number {
  const balance = this.getBalance();   // question
  this.chargeServiceFeeIfDue();        // action
  return balance;
}

Tests again. (Subtle point: we return the balance as it was before the fee, matching the original's behaviour exactly. Order matters when splitting — preserve the original observable behaviour to the letter.)

Step 4 — Migrate callers one by one, by intent. Visit each call site and ask: did this caller want the answer, the action, or genuinely both?

// Caller that only displayed the balance (most of them!):
const due = account.getBalance();
 
// Caller that ran the monthly fee job:
account.chargeServiceFeeIfDue();
 
// Caller that truly needed both, in the original order:
const due = account.getBalance();
account.chargeServiceFeeIfDue();

Run tests after each migration.

Step 5 — Delete the glue method. When no caller uses getBalanceAndChargeServiceFee() any more, remove it. The "And" name disappears from the codebase — always a good day.

Step 6 — Lock the door with a test. Add a regression test asserting the query is truly pure:

test("getBalance does not change state", () => {
  const account = makeAccountWithBalance(250);
  const first = account.getBalance();
  const second = account.getBalance();
  expect(second).toBe(first); // asking twice gives the same answer
});

Seen from above, the refactoring passes through four safe states — and just like the gradual rename, you may pause in any middle state without breaking a thing:

Figure 8: Four states of the split — each transition is test-guarded, and the combined method stays alive until the last caller chooses a side.
⚠️

Two traps deserve loud warnings. Trap one — hidden behaviour change: some old callers may have depended on the side effect without anyone knowing ("the balance screen is also what charges fees" — horrifying, but real systems work like this). If you migrate that caller to the pure query, fees silently stop being charged. That is why you migrate one caller at a time with tests after each, and why step 2 and 3 keep the combined method alive until every caller's true intent is confirmed. Trap two — concurrency: if two threads relied on the old method's read-and-update being one atomic step, your split just created a race window between the query and the command. For shared mutable state under concurrency, keep the atomic operation (this is the pop() exception) or add proper synchronization.

College corner: the property your new query gained has a precise name — idempotence (and beyond it, referential transparency). An idempotent read can be retried by any layer of the stack without harm, which is exactly why distributed systems demand it: networks fail mid-call, clients time out and retry, load balancers replay requests to a second server. If "read the balance" charges a fee, every retry is a new fee — a real category of production billing incidents. The deeper architectural cousin is CQRS (Command Query Responsibility Segregation), which takes Meyer's method-level rule and stretches it across services: a write model handles commands, a separate read model (often a different database, optimised for queries) handles questions. You cannot reason about CQRS until CQS at the method level is second nature — this refactoring is where that nature is built.

A Bigger Real-Life Example 🔐

A login system — the classic place where this bug bites hardest:

// BEFORE — "checking" a password punishes the account
class LoginService {
  // Returns whether login succeeded... and counts attempts... and locks accounts.
  validate(username: string, password: string): boolean {
    const user = this.users.find(username);
    if (!user) return false;
 
    const ok = hash(password) === user.passwordHash;
 
    if (!ok) {
      user.failedAttempts += 1;                  // side effect 1
      if (user.failedAttempts >= 3) {
        user.locked = true;                      // side effect 2
        this.mailer.sendLockoutWarning(user);    // side effect 3!
      }
      this.users.save(user);
    }
    return ok;
  }
}

Looks reasonable — until you see who calls it:

// The login controller — fine, this is the intended user:
if (loginService.validate(name, pw)) { startSession(); }
 
// The admin "test credentials" tool — oops, admins are locking customers:
const works = loginService.validate(customer.name, typedPassword);
 
// A unit test — runs validate 3 times, locks the fixture user, later tests fail mysteriously:
expect(loginService.validate("ravi", "wrong")).toBe(false);
expect(loginService.validate("ravi", "wrong")).toBe(false);
expect(loginService.validate("ravi", "alsowrong")).toBe(false); // ravi is now locked!

The question ("is this password correct?") and the policy action ("punish failed attempts") are welded together, so every asker becomes a punisher. Split them:

// AFTER — question and action, separated and named honestly
class LoginService {
  // QUERY — pure; safe for admin tools, tests, anyone
  isPasswordCorrect(username: string, password: string): boolean {
    const user = this.users.find(username);
    if (!user) return false;
    return hash(password) === user.passwordHash;
  }
 
  // COMMAND — the security policy, applied deliberately
  recordFailedAttempt(username: string): void {
    const user = this.users.find(username);
    if (!user) return;
    user.failedAttempts += 1;
    if (user.failedAttempts >= 3) {
      user.locked = true;
      this.mailer.sendLockoutWarning(user);
    }
    this.users.save(user);
  }
}
 
// The real login flow composes both, explicitly and readably:
const ok = loginService.isPasswordCorrect(name, pw);
if (ok) {
  startSession();
} else {
  loginService.recordFailedAttempt(name);  // the punishment is now a visible decision
}
 
// The admin tool asks freely — no customer gets locked:
const works = loginService.isPasswordCorrect(customer.name, typedPassword);
 
// Tests ask as many times as they like:
expect(loginService.isPasswordCorrect("ravi", "wrong")).toBe(false); // ravi unharmed

Look at what the split bought us. The security policy did not weaken — the login flow still counts and locks. But the decision to punish moved from a hidden corner inside a "check" into plain sight in the flow, where reviewers can see it, product can discuss it, and no innocent caller can trigger it by accident. The admin tool and the tests became trivially safe. And both methods finally have one-job names — no "And" needed.

The support team felt the difference immediately. Before the split, "mysteriously locked accounts" were a weekly ticket category — every admin credential check was secretly a punishment. After:

Figure 9: Surprise side effects show up as support tickets — separating the question from the punishment makes the category almost vanish.

The Same Refactoring in C# 🎯

The classic bank-balance example, with the overdraft fee:

// BEFORE — reading the balance can charge the customer
public class AccountService
{
    public decimal GetBalanceAndApplyOverdraftFee(Account account)
    {
        if (account.Balance < 0)
        {
            account.Balance -= 35m;   // side effect: Rs.35 overdraft fee
            _repo.Save(account);
        }
        return account.Balance;       // query: current balance
    }
}

The mobile app shows the balance on the home screen. Every screen refresh of an overdrawn customer charges another ₹35. Pull-to-refresh five times in frustration — ₹175 in fees. This exact shape of bug has happened in real financial systems.

The split, with each half honestly typed — query returns a value, command returns void:

// AFTER — CQS restored
public class AccountService
{
    // QUERY — no side effects; call it from screens, logs, anywhere
    public decimal GetBalance(Account account)
    {
        return account.Balance;
    }
 
    // COMMAND — state change, deliberate, returns nothing
    public void ApplyOverdraftFeeIfOverdrawn(Account account)
    {
        if (account.Balance < 0)
        {
            account.Balance -= 35m;
            _repo.Save(account);
        }
    }
}
 
// The nightly fee job — the ONE place that charges:
foreach (var account in overdrawnAccounts)
{
    accountService.ApplyOverdraftFeeIfOverdrawn(account);
}
 
// The app's balance screen — asks all day, harms no one:
var balance = accountService.GetBalance(account);

C#-specific notes:

  • The void return type on commands is itself documentation: nobody can accidentally use ApplyOverdraftFeeIfOverdrawn as a question.
  • C# 8+ lets you express query purity to tools with the [Pure] attribute (System.Diagnostics.Contracts), and readonly members on structs enforce no-mutation at compile time.
  • Watch out for properties: a property getter in C# looks like pure data access, so a getter with side effects is an even worse CQS violation than a method — every style guide forbids it. If you find get { _count++; return _count; }, this refactoring applies urgently.

A Quick Python Taste 🐍

The same split in Python, with the purity test that locks the door afterwards:

class KhataAccount:
    def __init__(self) -> None:
        self._entries: list[int] = []
 
    # QUERY — pure; safe in f-strings, logs, asserts, loops
    def balance(self) -> int:
        return sum(self._entries)
 
    # COMMAND — changes state, returns None on purpose
    def charge_service_fee_if_due(self) -> None:
        if self.balance() > 200:
            self._entries.append(10)
 
 
def test_balance_is_pure():
    account = KhataAccount()
    account._entries.extend([100, 150])  # balance 250, fee territory
    first = account.balance()
    second = account.balance()
    assert first == second == 250  # asking twice changes nothing

Python's convention of returning None from mutating methods is CQS folk-wisdom baked into the standard library: list.sort() sorts in place and returns None, while sorted(my_list) returns a new list and touches nothing. The language has been quietly teaching you Meyer's rule all along.

IDE Support 🛠️

Unlike Rename (F2 / Shift+F6) or Change Signature (Ctrl+F6), no IDE has a one-key "Separate Query from Modifier" button — the split requires human judgment about which lines are the question and which are the action. But the building blocks are fully automated:

ToolFeatureHow it helps here
JetBrains IDEsCtrl+Alt+M — Extract MethodSelect the side-effecting lines and extract them into the new command method in one step.
VS CodeCtrl+Shift+R — Refactor menu → Extract Method (language extensions)Same extraction for the command half.
All IDEsRename (F2 / Shift+F6)Give the final halves their honest names — query as a question, command as a verb.
Analyzers / linters.NET [Pure] + Code Contracts, ESLint plugins flagging side effects in gettersHelp detect impure "getters" and keep the query pure after the split.
Find Usages (Shift+F12 VS Code / Alt+F7 JetBrains)Lists every caller so you can migrate them one by one by intent.

So in practice the recipe is: Extract Method for the command, Rename for both halves, Find Usages for the migration — three automated tools orchestrated by one thinking human.

Benefits and Risks ⚖️

BenefitsRisks / Costs
Queries become safe everywhere — conditions, logs, asserts, debugger watches, dashboards — with zero fear of changing state.Atomicity loss: under concurrency, query-then-command opens a race window that the old combined method did not have. Keep pop()-style operations combined or synchronize.
Side effects become visible, deliberate lines of code that reviewers can see and question.A caller may have secretly depended on the side effect; migrating it to the pure query silently turns the effect off. Migrate one caller at a time, tests after each.
Queries can be cached, memoized, reordered, and called repeatedly — performance options that impure methods forbid.If the command computes the value expensively anyway, a separate query may duplicate work — measure before splitting hot paths.
Testing splits into easy halves: input/output checks for the query, state checks for the command.Two calls instead of one is slightly more code at sites that truly need both.
Each half earns an honest one-job name — no more "getXAndDoY". Pairs beautifully with Rename Method.Two state reads across a transaction boundary may observe different states than one atomic call would — respect transactional correctness.

Which Smells Does It Cure? 🧹

SmellHow Separate Query from Modifier helps
Mysterious Name (the "And" name)getBalanceAndApplyFee becomes two truthfully named methods; the "And" vanishes.
Long MethodA method doing two jobs (answer + action) splits into two short, single-purpose methods.
Side-effecting getters / surprising state changesThe defining cure: reads stop mutating; mutations stop hiding inside reads.
CommentsWarnings like // careful: also charges a fee! become unnecessary — the structure itself says it.
Temporal coupling between callers"Call this only once!" rituals disappear; queries are repeat-safe by construction.

Quick Revision Box 📦

+------------------------------------------------------------------+
|          SEPARATE QUERY FROM MODIFIER — CHEAT SHEET               |
+------------------------------------------------------------------+
| Story    : Asking "how much do I owe?" must NOT add chai to bill  |
| Principle: CQS — Bertrand Meyer (Eiffel, OOSC 1988)               |
|            "Asking a question should not change the answer."      |
| QUERY    : returns data, changes NOTHING observable               |
| COMMAND  : changes state, returns NOTHING (void)                  |
|                                                                   |
| SAFE STEPS:                                                       |
|   1. Create pure query (value logic only)                         |
|   2. Original uses query internally — test                        |
|   3. Extract command (side effects, void) — test                  |
|   4. Migrate callers BY INTENT: query / command / both            |
|   5. Delete the combined "And" method                             |
|   6. Add a test: calling the query twice changes nothing          |
|                                                                   |
| EXCEPTIONS: stack.pop(), queue.dequeue(), iterator.next(),        |
|             atomic fetch-next-id — keep atomic under concurrency  |
| TELLS     : "And" in the name | "don't call twice!" comments |    |
|             a get/is/has method that writes anything              |
+------------------------------------------------------------------+

Practice Exercise ✍️

A metro-card system has this method, and the operations team reports "balances drain mysteriously when staff check cards":

class MetroCard {
  private balance: number;
  private trips: Trip[] = [];
 
  // Returns remaining balance... and does WHAT else? Read carefully.
  checkBalance(gateId: string): number {
    if (this.balance < 20) {
      this.sms.send(this.ownerPhone, "Low balance! Please recharge."); // effect 1
    }
    this.trips.push(new Trip(gateId, new Date()));                     // effect 2 (!!)
    this.balance -= 1; // Rs.1 "balance inquiry charge"                // effect 3 (!!!)
    return this.balance;
  }
}

Tasks:

  1. List every side effect hiding inside this "check". For each, say who gets hurt when a station staff member checks a card five times. Which effect is the worst CQS violation, and why?
  2. Perform the full split, showing each intermediate state from Figure 8: (a) the pure getBalance() query, (b) the command(s) — decide whether the SMS warning, the trip logging, and the inquiry charge belong in one command or several (hint: do they always happen together for the same reason?), (c) the temporary glue method, (d) the final state after callers migrate.
  3. There are three callers in the codebase: the entry gate (which genuinely starts a trip), the staff handheld checker (which should only read), and the customer app (which should only read). Write the after-refactoring code for each caller, choosing query, command, or both. Then place each caller on Figure 3's pie — which slice does it belong to?
  4. Write the purity regression test from step 6 of the recipe for your new getBalance().
  5. Thought question: the entry gate must check the balance and deduct the fare as one step — if two people tap the same card at two gates in the same second, could your split create a race? Name the CQS exception this resembles, plot the scenario on Figure 4's quadrant, and suggest one way to keep that operation safe.
  6. Finally, rename check: do your finished methods need the word "And" anywhere? If yes, something is still glued together — split again.

When your staff checker can poke a card a hundred times and the balance does not move by even one rupee — the tea-stall notebook is honest again, Ramesh can ask his total every single day, and Bertrand Meyer would buy you a chai. On a separate, clearly itemised bill, of course.

Frequently asked questions

What exactly is Command-Query Separation (CQS)?
A design principle from Bertrand Meyer, introduced in his work on the Eiffel language and the book Object-Oriented Software Construction (1988). It says every method should be either a query (returns data, changes nothing observable) or a command (changes state, returns nothing) — never both. The one-line memory hook: asking a question should not change the answer.
How do I spot a method that violates CQS?
Three quick tells. The name needs an 'and' to be honest (getBalanceAndApplyFee). The method both returns a value AND assigns to fields, writes to a database, or sends messages. Or callers behave strangely around it — someone calls it 'just to read' and something changes, or someone avoids calling it twice 'because weird things happen'. Any of these means a query and a command are glued together.
Are there honest exceptions to CQS?
Yes, a few famous ones. stack.pop(), queue.dequeue(), and iterator.next() deliberately return a value and change state, because splitting them in concurrent code creates race conditions — another thread can sneak in between your peek and your remove. Fowler himself notes he is happy to break CQS for such cases. The principle is a strong default, not an iron law; deviations should be rare, well-known idioms.
Doesn't calling two methods instead of one make code slower?
Usually immeasurably. A pure query is often trivially cheap, and its results can now be cached safely precisely because it has no side effects. The real exception is when the command must compute the value anyway at significant cost, or when the query+command pair must be atomic under concurrency — in those cases, measure first, and keep them combined where correctness demands it.
Is CQS the same thing as CQRS?
Related but different scale. CQS is Meyer's method-level rule: one method is either a query or a command. CQRS (Command Query Responsibility Segregation) lifts the same idea to architecture: separate models — sometimes separate services and databases — for reading versus writing. Learn CQS first; CQRS is the same instinct applied to whole systems.

Further reading

Related Lessons