Lesson 13 of 54

Functions That Do One Thing

Command-Query Separation

Bertrand Meyer introduced a principle that sounds simple but changes how you think about function design:

A function should either do something (a command) or answer something (a query), but not both.

This is Command-Query Separation (CQS), and it's one of the most practical principles for writing predictable code.

Commands vs. Queries

Commands (Do Something)

Commands change state. They perform actions. They have side effects.

Javascript
saveUser(user);           // Changes database
sendEmail(to, body);      // Sends email
incrementCounter();       // Modifies state
deleteFile(path);         // Removes file
closeConnection();        // Releases resource

Commands typically return void (nothing) or a success/failure indicator. They don't return data.

Queries (Answer Something)

Queries return data. They don't change state. They're safe to call multiple times.

Javascript
getUser(id);              // Returns user
calculateTotal(items);    // Returns number
isValidEmail(email);      // Returns boolean
findOrdersByUser(userId); // Returns list
getCurrentTime();         // Returns timestamp

Queries are idempotent: calling them 10 times produces the same result as calling them once.

Why This Matters

Predictability

When you see a query, you know it's safe. It won't change anything. You can call it in debugging, in tests, in logging—anywhere.

Javascript
// Safe to add anywhere
console.log(getUser(id));  // I know this doesn't modify the user

When you see a command, you know to be careful. It has effects.

Javascript
// Need to think about this
deleteUser(id);  // This changes things!

The Problem with Mixing

When functions both do and answer, bugs follow:

Javascript
function getNextMessageId() {
  this.lastId++;
  return this.lastId;
}

Is this a query? The name says "get" (query). But it also increments (command).

Problems:

  • Calling it twice gives different results
  • You can't safely call it for logging or debugging
  • Tests have to account for side effects

Another Example

Javascript
function getUserAndUpdateLastAccess(id) {
  const user = db.users.find(id);
  user.lastAccessedAt = Date.now();
  db.users.save(user);
  return user;
}

This function:

  1. Reads from database (query)
  2. Updates timestamp (command)
  3. Saves to database (command)
  4. Returns user (query)

Now every time you need the user, you're also updating the database. Want to log the user's email? You just modified their record.

Refactoring to CQS

Before: Mixed

Javascript
function pop() {
  const item = this.items[this.items.length - 1];
  this.items.length--;
  return item;
}

pop is both a command (removes item) and a query (returns item). This is such common API that we accept it, but it does violate CQS.

After: Separated

Javascript
function peek() {
  return this.items[this.items.length - 1];  // Query
}

function removeLast() {
  this.items.length--;  // Command
}

// Usage
const item = peek();
removeLast();

Real-World Example

Before:

Javascript
async function processOrder(orderId) {
  const order = await db.orders.findById(orderId);
  order.status = 'processing';
  await db.orders.save(order);
  return order;  // What state is this in?
}

After:

Javascript
// Query: just returns the order
async function getOrder(orderId) {
  return await db.orders.findById(orderId);
}

// Command: just updates the status
async function markOrderProcessing(orderId) {
  await db.orders.updateStatus(orderId, 'processing');
}

// Usage
const order = await getOrder(orderId);
await markOrderProcessing(orderId);

Now each function does one thing. getOrder is safe to call anywhere. markOrderProcessing clearly signals that it changes state.

When to Break the Rule

CQS is a principle, not a law. Sometimes combining makes sense.

Returning Created Resources

Javascript
async function createUser(userData) {
  const user = { ...userData, id: generateId() };
  await db.users.insert(user);
  return user;  // Return what was created
}

This is a command (creates user) that returns data (the created user). It's pragmatic—the caller usually needs the created entity, especially the generated ID.

Stack/Queue Operations

Javascript
const item = stack.pop();  // Command + Query

This is so idiomatic that separating it would be pedantic.

Atomic Operations

Javascript
const oldValue = counter.getAndIncrement();

Sometimes you need the old value atomically. Separating into "get" then "increment" introduces race conditions.

The Guideline

When combining is acceptable:

  1. The return value is directly related to the mutation (e.g., returning the created entity)
  2. Separating would create race conditions
  3. The combination is idiomatic and widely understood

When combining is not acceptable:

  1. The side effect is hidden (the name doesn't reveal it)
  2. The query is useful independently of the command
  3. The combination surprises readers

Testing Benefits

CQS makes testing easier:

Javascript
// Testing queries: no mocking needed for side effects
test('calculateTotal returns sum of item prices', () => {
  const items = [{ price: 10 }, { price: 20 }];
  expect(calculateTotal(items)).toBe(30);
});

// Testing commands: clear expectations
test('deleteUser removes user from database', async () => {
  await createUser({ id: 1, name: 'Test' });
  await deleteUser(1);
  expect(await getUser(1)).toBeNull();
});

Queries are pure functions (or close to it). Commands have clear effects to verify.


Key insight: Functions should either change state (commands) or return data (queries), not both. Queries are safe to call anywhere. Commands signal that caution is needed. When you mix them, you create surprises and bugs.