Applying Command Query Separation (CQS) for Cleaner and More Predictable Code

The Command Query Separation (CQS) principle is a powerful guideline in software development, particularly in designing APIs and methods. It clarifies the purpose of functions and methods by defining two distinct types of operations: commands and queries. Proposed by Bertrand Meyer, CQS establishes that “asking a question should not change the answer.” This article will explore how to implement and adhere to CQS in Angular with TypeScript examples.

Vítor Azevedo
5 min readNov 3, 2024

Key Takeaways

  • Commands modify state; queries provide information without changing it.
  • Separating commands from queries clarifies intent, making code behavior predictable.
  • CQS enhances readability and reduces unexpected side effects, leading to safer code.

Understanding CQS

In the CQS principle:

  • Commands are operations that change the state of the application but return no data.
  • Queries are operations that return data but do not modify the application’s state.

By clearly defining and separating these two types of operations, CQS prevents unintended side effects, making code behavior predictable. This is particularly beneficial in complex applications where separating data retrieval and state modification reduces ambiguity and makes debugging easier.

Example of CQS in TypeScript

Let’s break down how CQS would look in Angular using TypeScript. We will create two separate methods: a command to update the state and a query to retrieve data.

Creating a User Service

Consider a simple service that manages user information:

// user.service.ts
import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root',
})
export class UserService {
private userList: { id: number; name: string }[] = [];

// Command: adds a new user to the list, does not return anything
addUser(id: number, name: string): void {
this.userList.push({ id, name });
}

// Query: retrieves the list of users, does not modify the state
getUserList(): { id: number; name: string }[] {
return [...this.userList]; // Returns a copy to ensure immutability
}
}

In this example:

  • The addUser method is a command that changes the state by adding a user but returns nothing.
  • The getUserList method is a query that returns the list of users without altering the internal state.

Applying CQS in Angular Components

Angular components often rely on services for state management. Using CQS in these components helps maintain clean separation between commands and queries.

// user.component.ts
import { Component } from '@angular/core';
import { UserService } from './user.service';

@Component({
selector: 'app-user',
template: `
<div *ngFor="let user of users">
{{ user.id }} - {{ user.name }}
</div>
<button (click)="addNewUser()">Add User</button>
`,
})
export class UserComponent {
users = this.userService.getUserList(); // Query: getting data without changing state

constructor(private userService: UserService) {}

// Command: adds a new user, causing a state change
addNewUser(): void {
const newId = this.users.length + 1;
const newName = `User ${newId}`;
this.userService.addUser(newId, newName);
this.users = this.userService.getUserList(); // Refreshes the view with updated data
}
}

In this example:

  • getUserList is a query and is used only to retrieve data.
  • addNewUser is a command that modifies the user list. By separating these operations, the component maintains a clear distinction between actions that alter data and those that merely read it.

Visualizing CQS in Angular

Imagine a simple interface where a command button adds users, and a list query displays them. Commands here are distinctly different from queries, ensuring that every button click triggers an action (add a user) without returning data, while the displayed list reflects current state without altering it.

When to Break CQS: Practical Exceptions

While CQS is highly beneficial for clarity and predictability, there are scenarios where it may be reasonable to combine commands and queries. Here are a few common exceptions:

  1. Data Structures with Dual-Function Methods: Some data structures, such as stacks and queues, naturally mix commands and queries. For example, pop removes and returns the last item in a stack.
  2. Transaction Scripts: In some cases, scripts or methods need to handle complex transactions where both a state change and a return value are necessary, like saving data and returning the created entity.
  3. I/O Operations: Input/output operations may involve both a state change and a return value, as in the case of reading from a file stream, which returns data while updating the read pointer.
  4. Fluent Interfaces: Fluent APIs sometimes allow method chaining by returning the object itself after a state change, which can improve readability without strictly adhering to CQS.
  5. Error Handling and Recovery: When an operation could fail, it may be necessary to perform a rollback or cleanup and return error information, combining state changes with status or error messages.
  6. Initialization: Methods that initialize an object’s state and provide feedback, such as factory methods that return newly created instances, may break CQS for practical reasons.

Practical Exception Example in Angular

Suppose we have a pop method on a custom stack service that both removes and returns the top element. While this breaks the strict CQS, it can be useful in scenarios where removing and getting an element in one step is required.

// stack.service.ts
import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root',
})
export class StackService {
private stack: number[] = [];

// Command and Query combined: removes and returns the top item
pop(): number | undefined {
return this.stack.pop();
}

// Command: adds an item to the stack
push(item: number): void {
this.stack.push(item);
}

// Query: returns current stack state without modification
getStack(): number[] {
return [...this.stack];
}
}

In this example:

  • The pop method performs both a command (modifying state) and a query (returning data), which is acceptable for stack structures.
  • The push and getStack methods adhere strictly to CQS.

Benefits of Following CQS

  1. Enhanced Readability: Code is easier to read and understand when methods have a clear purpose, either performing an action or retrieving data.
  2. Improved Maintainability: By avoiding methods that mix state changes with data retrieval, developers reduce unexpected side effects and make it easier to maintain code over time.
  3. Better Testability: Commands and queries are straightforward to test independently, with commands focused on effects and queries on outputs.

Conclusion

The CQS principle is a fundamental concept in software design that simplifies code by separating state-changing commands from data-retrieving queries. While it may not be practical to strictly adhere to CQS in every situation, using this principle where possible helps create predictable, readable, and maintainable code in Angular applications. Exceptions to CQS should be made thoughtfully, with careful consideration of whether combining commands and queries genuinely enhances clarity or functionality.

By implementing CQS, Angular developers can ensure that their applications are easier to understand, debug, and maintain, paving the way for clearer, more reliable code in complex software systems.

--

--

Vítor Azevedo
Vítor Azevedo

Written by Vítor Azevedo

Frontend Developer with 25+ years' expertise in HTML, CSS, JavaScript, Angular and Vue. Builds dynamic, user-centric web apps. Award-winning projects.

No responses yet