Mathieu Eveillard

Command-Query Responsibility Segregation

CQRS is based on the idea of distinguishing between write and read models when operations are highly asymmetric. Use sparingly.

Command-Query Responsibility Segregation

We’ve been hearing a lot about it for several years now. On paper, it’s not very complicated. In practice, it’s a different story. CQRS, which stands for Command Query Responsibility Segregation, is a tool that must be used wisely, and it all comes down to the specific use case.

Let’s start with an example to fully understand the problem that CQRS solves.

4, 8, 15, 16, 23, 42…

Imagine a REST API exposing two routes:

  • A POST method that lets you save a single number: 4, 8, 15, 16, 23, 42…
  • A GET method that allows you to retrieve the average of all the numbers stored: 4.0, 6.0, 9.0, 10.8, 13.2, 18.0…

You might say the functionality isn’t anything to write home about, but it has the merit of being simple.

Now let’s add a constraint: the ability to explain the value of the average calculated this way if an audit procedure were to be triggered. The only way to explain the value 18.0 is to show the auditor a sequence of numbers (4, 8, 15, 16, 23, 42) whose average is 18.0.

I’ll skip the implementation of the controllers so we can focus on the underlying data model. Let’s start with a naive implementation: type Model = number; in TypeScript. This model is naturally suited for POST requests, but GET requests may become problematic as the volume of data increases.

Just imagine calculating the average of 100,000 numbers. Regardless of which layer handles it (application layer or persistence layer), in the end someone will have to add up 100,000 values and count them to divide by 100,000. There must be a better way.

The idea, then, is to create two separate models: a write model and a read model. This could look like this, still in TypeScript:

// WriteModel
type WriteModel = {
  value: number;
};

// ReadModel
type ReadModel = {
  count: number;
  sum: number;
};

const computeAverage = ({ count, sum }: ReadModel): number => sum / count;

// WriteModel -> ReadModel synchronization
const synchronize =
  (value: number) =>
  ({ count, sum }: ReadModel): ReadModel => ({
    count: count + 1,
    sum: sum + value,
  });

The WriteModel is very similar to the naive model. In the database, you’ll find the sequence of values: 4, 8, 15, 16, 23, 42. The ReadModel, on the other hand, is truly new, since it will result in two database records: 108 (the sum of the values) and 6 (the number of values). All that remains is to perform the division at the very end (1).

In doing so, we’ve reinvented CQRS.

Definition of CQRS

CQRS can be considered in cases where write and read operations are asymmetric. This may be because the operations are very different in nature, or because they involve different workloads and therefore very different scalability requirements (many more calls on one side than the other).

In such a situation, rather than a general-purpose model that does a little of everything but really nothing well, CQRS opts to distinguish between two models, which can diverge and specialize:

  • A model for writing = Command: we command the system to do something (2);
  • A model for reading = Query: we execute a query to determine the system’s state.

With the necessary synchronization between the two models.

Note in passing: separate models imply a change of context (a concept from Domain-Driven Design):

  • A write context: put that way, it might sound a little odd (writing just to never read it?), but in fact there is a purpose for reading it: it’s for traceability and proof. Without it, the command would feed directly into the read model, and there would be only one model.
  • A read context: our initial need.

So, at its core, CQRS is really just one heuristic among many for breaking things down into bounded contexts.

Since the models are separate, so is their persistence. Depending on your deployment model, this means either separate databases (microservices) or separate schemas within the same database (modular monolith).

Let’s now take a look at a common misunderstanding that makes CQRS difficult to understand.

CQRS ≠ Event Sourcing

CQRS and Event Sourcing (ES) are often presented together, to the point where it can be difficult to tell them apart. However, each architecture has its own distinct identity:

  • CQRS involves separating write and read models to improve performance. As we’ve seen, CQRS is an architectural pattern that, by definition, involves two contexts.

  • Event Sourcing, on the other hand, is an append-only operating mode implemented within a single context. Any change to the system’s state results in an event, and the sequence of these events constitutes the single source of truth: a fact is a fact; it cannot be undone. Example: your bank account.

CQRS and ES are often associated, and rightly so, because the two work well together. Event Sourcing then serves as the write model.

But one doesn’t necessarily imply the other:

  • It’s possible to use ES without CQRS. It’s rare, though, because ES quickly leads to performance issues (as you can imagine, your bank account balance isn’t calculated on the fly since the account was opened in 1999 🤣);

  • Conversely, we could implement CQRS without ES, but frankly, that’s also pretty theoretical: I’m actually having a hard time coming up with an example.

Conclusion

In any case, keep in mind that implementing a CQRS pattern introduces real complexity, particularly due to model synchronization. By trying to solve one performance problem, you end up creating others. The initial problem must therefore be truly significant and justify this additional complexity.

(1) The synchronization mechanism proposed here is rather naive. A real-world implementation would likely involve publishing an event and setting up a processing queue.

(2) We tend to think in terms of commands to better “capture” the business intent, which ultimately results in a write. However, the CQRS principle would remain unchanged if this write were the result of an API call without any specific formalism.

← All articles