# Separate Query from Modifier

> Split a method that both returns a value and changes state into a side-effect-free query and a separate command.

- kind: technique
- category: simplifying-method-calls
- aliases: Command-Query Separation
- canonical: https://aipatterns.dev/refactoring/techniques/separate-query-from-modifier
- inspired-by: https://refactoring.guru/separate-query-from-modifier

## Problem

A method returns information and also changes state. Callers who only want the information trigger the side effect anyway. Calling it twice, in a log line, a debugger watch or a test assertion, changes the outcome.

## Solution

**Create one method that only answers the question and one that only performs the change. Callers who need both call the modifier, then the query.**

## Before

```ts
class RateLimiter {
  private hits = new Map<string, number[]>();

  // Returns remaining requests in the window, and records this request
  remaining(apiKey: string, now: number): number {
    const windowStart = now - 60_000;
    const recent = (this.hits.get(apiKey) ?? []).filter((t) => t > windowStart);
    recent.push(now);
    this.hits.set(apiKey, recent);
    return Math.max(0, 100 - recent.length);
  }
}

if (limiter.remaining(key, Date.now()) === 0) throw new TooManyRequests();
res.setHeader("X-RateLimit-Remaining", limiter.remaining(key, Date.now())); // counts twice
```

## After

```ts
class RateLimiter {
  private hits = new Map<string, number[]>();

  recordHit(apiKey: string, now: number): void {
    const windowStart = now - 60_000;
    const recent = (this.hits.get(apiKey) ?? []).filter((t) => t > windowStart);
    recent.push(now);
    this.hits.set(apiKey, recent);
  }

  remaining(apiKey: string, now: number): number {
    const windowStart = now - 60_000;
    const recent = (this.hits.get(apiKey) ?? []).filter((t) => t > windowStart);
    return Math.max(0, 100 - recent.length);
  }
}

limiter.recordHit(key, Date.now());
const left = limiter.remaining(key, Date.now());
if (left === 0) throw new TooManyRequests();
res.setHeader("X-RateLimit-Remaining", left);
```

## Why

- Queries become safe to call any number of times, from anywhere, including tests and logs.
- The call site shows where state changes, which makes ordering bugs visible.
- The double-counting bug in the "before" code cannot be written by accident.

## Steps

1. Create the query: copy the method, remove every statement that changes state, and give it a name for the value it returns. Run the tests.
2. Make the original method call the query for its return value, so the computation exists once. Run the tests.
3. At each call site, replace `x = original()` with `modifier(); x = query();`. Run the tests after each site.
4. Change the original method to return `void` and rename it for the action. Run the tests.
5. Extract shared logic between the two (here, the window filter) with [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md).

## Watch out

- Do not split operations that must be atomic under concurrency. `pop()`, `compareAndSet`, `INCR` in Redis and `nextval` in SQL return and mutate in one step on purpose. Splitting them creates a race between the check and the change.
- Do not split when the "side effect" is invisible caching or memoization. That is still a pure query from the caller's view.
- Splitting can double the cost of expensive work. Share an intermediate value or cache it if profiling shows a problem.
- There is no inverse technique; merging query and modifier is usually a regression.

## Related

- Shortens [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md) and clarifies intent.
- Lets you use [Replace Temp with Query](https://aipatterns.dev/refactoring/techniques/replace-temp-with-query.md) freely, since queries are safe.
- Often followed by [Rename Method](https://aipatterns.dev/refactoring/techniques/rename-method.md) on the modifier.

## Agent checklist

- [ ] The new query has no observable side effects.
- [ ] Every call site that needed both now calls modifier then query, in that order.
- [ ] The operation does not need to be atomic across threads, processes or requests.
- [ ] Tests that asserted on the return value of the modifier now use the query.
