Home » Event Sourcing 🌟 AI Research · Newsletter · ML Labs · About

Command-Query Separation

Command-query separation (or CQS for short) is a design principle devised by Bertrand Meyer. CQS states that every method should either be a command that performs an action (changes state), or a query that returns answer to the caller (without changing the state or causing side-effects), but not both.

In other words, functions should return a value only if they are pure (don't make any visible state changes). This convention, if followed consistently, can simplify programs, making them easier to understand and reason about.

Following method violates CQS principle, it does too much:

func SaveUser(name string, age int) *Result {
    if name == "" {
        return NewError("Name is empty")
    }
    if age <= 0 || age >= 100 {
        return NewError("Age is out of range")
    }
    users = append(users, NewUser(name, age))
    return nil
}

We can simplify this code by breaking it into two methods. Command:

func CreateUser(name string, age int) {
    users = append(users, NewUser(name, age))
}

and query:

func ValidateUser(name string, age int) *Result {
    if name == "" {
        return NewError("Name is empty")
    }
    if age <= 0 || age >= 100 {
        return NewError("Age is out of range")
    }
    return nil
}

We can reuse and test these methods separately or compose them. For example, to print validation errors:

func InputChanged(name string, age int) {
    var result = ValidateUser(name, age)
    if !result.Valid {
        RenderErrors(result)
    } else {
        RenderSaveButton()
    }
}

Saving user while safe-guarding against invalid input:

func SaveButtonClicked(name string, age int) {
    var result = ValidateUser(name, age)
    if !result.Valid {
        // protect from programmer error
        panic(result)
    }
    SaveUser(name, age)
}

Following CQS principle is a good guideline for a project coding style. However, it might be unfeasible to follow it everywhere. Instead, we could be consistent within a context. For example: accept that UI is messy but keep backend clean.

Exceptions

CQS works best when it we treat it as a design principle. If used consistently, it helps people understand code better and faster.

Yet, there always are exceptional edge cases, where it could be wise to step away from such principle:

  • Operations related to concurrency, where it is important to both mutate state and get the result back: Interlocked.Increment or sync.Add.

  • Well-established data structures, where functions with result and side-effect are common: queue.Dequeue or Stack.Pop

It is possible to modify all of these methods to follow CQS principle. Yet, that would introduce additional complexity and deviate from the expected behavior.

It might be better to accept the deviation and note it explicitly (e.g. in documentation, inline comments or method names).

CQS in Languages

CQS is prominent in Functional Programming. Languages supporting it often have this principle baked right into the language design itself.

Haskell and F# emphasize functional programming. Learning them would be a good exercise and can make you a better developer.

Languages following Object-Oriented Programming approach don't enforce CQS by default. Yet, following these principles can lead to simpler code that is more predictable and easy to reason about.

Some ecosystems offer additional tooling to make CQS principle more explicit to the developers. For example, Microsoft Code Contracts introduces Pure Attribute to indicate methods that don't have visible side effects.

References

  1. Command-query separation - Wikipedia
  2. Pure Attribute

Published: December 01, 2014.

Next post in Event Sourcing story: Event Sourcing a la Lokad

🤗 Check out my newsletter! It is about building products with ChatGPT and LLMs: latest news, technical insights and my journey. Check out it out