Command Query Separation (CQS) states that every method should either be a command that performs an action or a query that returns data, but not both.

Core Concept

  • Queries: Return data and have no side effects
  • Commands: Perform actions and modify state, but return nothing (or only success/failure)

Benefits

  • Predictability: Queries can be called safely multiple times
  • Testability: Commands and queries can be tested separately
  • Reasoning: Easier to understand method purposes
  • Caching: Queries can be safely cached

Example

Violates CQS

// BAD: Both queries and modifies state
public class Stack
{
    public int Pop()
    {
        // Gets value (query) AND removes it (command)
        int value = _items[_top];
        _items[_top] = default;
        _top--;
        return value;
    }
}

Follows CQS

// GOOD: Separate query and command
public class Stack
{
    // Query: Returns data, no side effects
    public int Peek()
    {
        return _items[_top];
    }
 
    // Command: Modifies state, returns nothing
    public void Pop()
    {
        _items[_top] = default;
        _top--;
    }
}
 
// Usage
int value = stack.Peek();  // Query
stack.Pop();               // Command

Practical Challenges

Server-Generated IDs

When creating entities, you often need the generated ID:

// Pragmatic violation of CQS
public class UserRepository
{
    // Returns ID (query) after creating (command)
    public Guid Create(User user)
    {
        _db.Users.Add(user);
        _db.SaveChanges();
        return user.Id; // Need this for further operations
    }
}

This is a known pragmatic exception - sometimes you need confirmation of what was created.

Alternative: Use Return Values for Success/Failure

// Command can return success indicator
public bool TryPop(out int value)
{
    if (_top < 0)
    {
        value = default;
        return false;
    }
 
    value = _items[_top];
    _items[_top] = default;
    _top--;
    return true;
}

CQRS Extension

Command Query Responsibility Segregation (CQRS) extends CQS to the architectural level:

  • Separate read models from write models
  • Different data structures for queries vs commands
  • Often used with event sourcing

See read models and boundaries for CQS taken to the architectural level: a single write door, and inert value objects (not live records) crossing boundaries.

Resources