The CQRS pattern was described by Greg Young in 2010 as a way to separate writing patterns (commands) and reading (queries) of our applications, thus allowing an asymmetric scaling between the read and write mechanisms of our application.
The basic idea is that we can use a model for writing, different from the model we use for executing readings, as a counterpoint to the usual CRUD (Create, Read, Update, Delete). It is worth noting that this approach, the CQRS approach, can add more complexity than a simple system based on CRUD.
Implementation of CQRS
Separation of models
We can implement CQRS in different ways depending on our needs.
For example, we can choose to maintain a single database and perform only one separation at the connection level, i.e., we will use a different connection for the read operations (queries) and those of writing (commands). This allows us to configure different parameters such as timeouts, The size of requests and other parameters are more in line with the nature of the operation.

This approach has the disadvantage that the separation only exists at a logical level, i.e., we are still attacking the same database, so it is still the same system that takes on the workload.
Another approach would be to use two separate databases for writes and reads:

In this way, the load of read and write operations is more evenly distributed and not only at a logical level, but two different systems handle this load. The disadvantage of this approach is that it creates a eventual consistency, In other words, the read and write models will not be consistent at all times, but will eventually differ from each other, which can create a problem depending on the nature of the data.
Implementation of the models
By implementing read and write operations we will have 3 new pairs of actors:
- Query y QueryHandler.
They are, respectively, the reading model and its handler. The query is nothing more than a DTO with all the necessary information to perform the reading (parameters, filters, pagination...). On the other hand, the handler or handler is the service in charge of obtaining this information and returning it to the controller.
As a general rule, read operations are always synchronous.
The correspondence of queries and handlers is 1:1, that is, each query has only one handler, it cannot be handled by more than one.
- Command y CommandHandler.
They are respectively the writing model and its handler. The command is nothing more than a DTO with all the information necessary to perform the write (identifier of the record(s) to be edited, attributes to be modified...). On the other hand, the handler or handler is the service in charge of persisting this information.
Write operations can be synchronous or asynchronous, which represents an advantage in terms of efficiency, since we can simply launch the update action without waiting for the update to finish: the frontend already has the updated data, since he is the one who sent them to us.
We must have, however, mechanisms to react to asynchronous data writing problems (retry system, notifications to the frontend via server, etc.). push o sockets...).
The correspondence of commands and handlers is 1:1, that is, each command has only one handler, it cannot be handled by more than one.
- Event y EventHandler.
In this case, they are, respectively, the model of something that has happened in our system and its driver. The event is nothing more than a DTO with the information of what has happened, it is usually composed of a date or timestamp of when it happened, a descriptive name, always in the past tense, of what happened (for example: user_deleted) and a body or payload with all relevant information to manage the event. This payload can be composed of data necessary to identify the affected record, the attributes that have been modified, etc.
Like the commands, In addition, events can be handled synchronously or asynchronously, ensuring that the same precautions are taken to handle possible errors.
Contrary to the previous cases, the correspondence of events and handlers (or listeners) is 1:n, that is, each event has more than one handler or listener.
A very common use case for events is to eliminate the eventual consistency produced in systems with separate databases: after an update, deletion or insertion of data in the write database, an event alerts the changes so that they are replicated in the read database.

Synchronous or asynchronous handling of models
To enable synchronous or asynchronous handling of the models we usually make use of an intermediate system that is responsible for matching the commands, the queries and the events with their respective handlers. This intermediate system is what we call a bus. The bus is like a queue where we put the commands, queries o events and these are executed by their handlers partners. This bus can be implemented with systems such as Message Queues Systems (RabbitMQ) or systems such as Redis that allow the gluing of tasks.

The advantage of the use of buses in the form of message queues is that we add a layer of indirection between action and execution. This means that the queued task can be consumed by our own system or by an external system that connects to the queuing service, allowing us to have external systems in charge of handling, for example, eventual consistency, or to delegate long and heavy tasks to systems expert in performing these same tasks, which may not even be written in the same language as our main system.

Let's see an example of how our system could handle eventual consistency (without the use of external agents):

In the example above we see how the command handler triggers an event to the event bus. This event may be the notification that a new record has been written, or that a previously existing one has been edited or deleted. It may represent more things, such as that you have subscribed to a service, that you have made like to a photo... whatever. The point is that this event is captured by the associated handler and acts accordingly by updating the read database, trying to reduce the eventual consistency time as much as possible.
Advantages of CQRS
- Asymmetric scaling.
In high availability environments, where there are thousands of interactions per second and there are flows with more traffic than others, the asymmetric scaling of our platform is something really desirable, since we can focus the economic effort on that part only. In the case of CQRS, having separate write and read domains allows us to focus our economic efforts on the one that requires more investment.
Do we have a social network type project where people spend more time reading content than producing it? We increase the instances of reading, we scale the instances... whatever it takes, but just focus on the reading ones.
Is our project like an API where sensors across hundreds of installations write logs every second or minute? We focus on scaling the writes, since there are probably few people consulting the read panel compared to the number of writes. - More semantic domain separation.
This advantage is more at an organizational level, but the fact of having our use cases separated between reads and writes simplifies a lot the understanding of it. For example, we know for sure that a read is not going to have side effects or that we can run it as many times as we want without repercussions. Then, the separation of the side effects that scripts can produce, handling with events allows us to focus the use case on its unique purpose, and delegate these side effects to other portions of the code, making everything much cleaner and more organized.
Disadvantages of CQRS
- Complexity.
The fact of having to implement CQRS significantly increases the complexity of our system, both for having twice the number of configurations or database instances, as well as for the need to use buses (although this is really optional). If we are not used to working in layers it can be a really abrupt change, although in the medium/long term the situation improves ostensibly.
A problem derived from the complexity is that we need a senior team trained to design our strategy and set up a workflow that can then be followed by our more junior colleagues, but a good design can lead to an almost mechanical process to implement new use cases based on commands y queries. - Eventual consistency.
This is the real big drawback of CQRS, since, depending on how urgent it is to have our data updated, it can be a good option or not, because we will have to guarantee a minimum time for both databases to be consistent. Then, as a result of this, there may be problems with the delivery of data from the buses, The fact that they may arrive repeated or out of order is something that we also have to control.
The following is a technique to facilitate the management of eventual consistency, called Event Sourcing.
Eventual consistency and Event Sourcing
Event Sourcing is a pattern or technique that we can use to try to simplify eventual consistency issues, as well as add a number of benefits to our system, such as:
- Observability. We know why a registry is in the state it is in.
- Fault tolerance. We can reconstruct the logs from the saved events.
- Audit. Closely related to observability, it allows us to audit the actions taking place in our system and we can determine erroneous data flows.
What does the Event Sourcing?
It is a pattern by which we record in a separate database or event store all the events that occur in our system, so that they form a history of changes associated with a record in our database. This allows us to reconstruct a record at any time in the life flow of our system.

Events, of course, must store the date on which they were triggered, since this is what will determine their order in the chain. It is also important that they store the identifier of the database record to which they belong, so that when we want to recreate a record or scroll to a point in time, it is necessary to be able to identify the record in question.
The event sourcing only applies to write models, since reads do not affect the status of the registers.
What does CQRS bring to us?
For us CQRS has been a change for the better, as it has allowed us to establish a common way of working that greatly simplifies the understanding of projects, and this includes when we move between projects in which we have never participated before.
Although at first sight it is perceived as a complex system (and it is), the advantages it brings us in terms of stability, maintainability and scalability of our code (as well as the aforementioned ease of rotation between projects), far outweigh the disadvantages.
It is worth mentioning that in smaller projects we have made decisions that simplify everything or that cancel out some of the inconveniences, such as using synchronous commands or directly eliminating the buses from queries and of commands, The new system, which erases a large part of the problems in one fell swoop.
Adapting the patterns to the needs of each project is also a strategic decision that must be made, since we must be sure of its implications and the ease of reversing these adaptations, because if a project grows, we may need those extras that the original pattern provides.
_ Bibliography
CQRS Documents. Greg Young (2010)
CQRS. Martin Fowler (2011)
CQRS By Example. Carlos Buenosvinos and Christian Soronellas (2022)
Domain-Driven Design in PHP. Carlos Buenosvinos and Christian Soronellas (2016)
Event Sourcing made easy.
