Skip to content

Separate Query from Modifier

When one function answers a question and changes the world as a side effect, callers lose the freedom to merely ask. Pull the two jobs apart: a query that returns a value and touches nothing, and a command that performs the change and returns nothing. Now a caller who only wants the answer can call the query without triggering the side effect.

You want to call a function just to read its result — maybe in a test, a log line, or an assertion — but you cannot, because calling it also sends an email, mutates a field, or advances a cursor. The signature lies about its cost: it looks like a question but it is secretly an action. Any function that returns a value yet also has an observable side effect is a candidate.

A function scans scores, returns the highest, and — buried inside — fires an alert when the high score crosses a threshold. We split it into a pure highScore query and an alertOnHighScore command.

// Before
function findHighScore(scores: number[]): number {
let highest = 0;
for (const s of scores) {
if (s > highest) highest = s;
}
if (highest > 9000) {
sendAlert(`New record: ${highest}`);
}
return highest;
}
// After
function highScore(scores: number[]): number {
return scores.reduce((highest, s) => (s > highest ? s : highest), 0);
}
function alertOnHighScore(scores: number[]): void {
if (highScore(scores) > 9000) {
sendAlert(`New record: ${highScore(scores)}`);
}
}
flowchart LR
  subgraph Before["Before"]
    A["findHighScore()<br/>returns the max<br/>AND logs an alert"]
  end
  subgraph After["After"]
    B["highScore()<br/>pure query"]
    C["alertOnHighScore()<br/>command"]
  end
  Before -.->|"Separate Query from Modifier"| After
One mixed function becomes a pure query plus a command
  1. Copy the function and name the copy as a pure query — by convention a noun or a get/is-style name that promises only an answer.
  2. Strip every side effect out of the query copy, leaving only the value it computes. Have it return that value.
  3. Find each caller of the original. Where the caller used the return value, switch it to the query.
  4. Leave the original function doing only its side effect. Inside it, call the new query rather than recomputing the value.
  5. Run your tests after each caller is moved across.
  6. If the original now returns a value nobody uses, change it to return nothing so the command’s signature is honest.

Apply this whenever a function with a return value also mutates state, performs I/O, or fires events — and especially before you write tests, since a pure query is trivial to assert on. The clean separation also makes the query safe to call repeatedly or cache.

The cost is that a caller who genuinely wanted both behaviours now makes two calls, and the value may be computed twice. That recomputation is usually cheap; if it is not, compute once and pass the result. Some operations are inherently atomic — a pop that returns and removes in one step — and forcing them apart would invite race conditions. There, keep them together on purpose.

What is the defining trait of a function that needs Separate Query from Modifier?
After the split, what should the pure query do?
Which principle does this refactoring put into practice?
When is it acceptable to keep query and modifier together?