The formerly exotic architectural pattern CQRS is becoming increasingly suitable for the masses. However, too many developers still know the approach only from hearsay, and can hardly estimate whether it is worth the effort.
Until a few years ago, when searching for CQRS, one was asked by Google whether one might have meant the search term cars. In the course of time, this has developed into a joke that developers familiar with CQRS actually pronounce the acronym CQRS like cars. But what is CQRS anyway?
One of the simplest and most frequently cited explanations is that CQRS is in principle the same as the design pattern CQS, applied to the architectural level of an application. This comparison is quite correct, but for someone who is not yet familiar with CQRS, it is difficult to understand and therefore hardly helpful.
The fundamental question must therefore first of all be what the design pattern CQS actually is. Bertrand Meyer's approach separates the methods of objects into two categories: Commands and queries. This is where the name comes from, because CQS stands for Command Query Separation.
Commands and queries
A command is a method that either changes the state of an object, has side-effects, or fulfills both criteria at the same time. However, a command deliberately does not return a return value, which is why it cannot return any information about the state of an object.
A query, on the other hand, is a method that returns information about the state of an object, but must not influence this state or have any other side effects.
According to CQS, you should be able to classify each method of an object in exactly one of the categories. Methods that change the state and have side-effects, and at the same time return information about the state, should therefore be avoided.
At first glance, meeting the requirement seems trivial. Considering classic
set methods, it is clear that some are queries and others are commands. However, the practice knows more advanced methods that can no longer be assigned so easily.
For example, a method that saves a file and at the same time returns the number of bytes written would be questionable. Saving the file is a side effect, so it is a command. However, since it also returns the number of bytes written, this is also a query. How can this case be dealt with if the CQS principle is to be observed?
An interesting approach is suggested by Yegor Bugayenko in his book Elegant Objects: Instead of creating the method as outlined above, you should reduce it to a query that returns a dedicated object representing a one-time save:
// Query const saver = file.getSaver(); // Command saver.save(); // Query const bytesWritten = saver.getBytesWritten();
This new object then has the actual method
save, which is now a command, and the method
getBytesWritten as query. In this way, the duality of the original method can be resolved into two separate methods as a command and query.
The reason why the procedure works in the described way is the separation of writing and reading, even in a process that supposedly does both at the same time.
Separating writing and reading
The CQRS design pattern raises the idea of separating writing and reading data from object to system level. This means, for example, that an application has not only one but two APIs to address it: While one API is used for writing data, the other is used for reading.
The separation does not necessarily have to be technical, but at least it should be thoughtfully planned. At first glance, this seems absurd and looks like unnecessary effort. In fact, however, the approach does offer some serious advantages.
A typical problem for applications that are subject to a high load is, for example, normalizing the database. For writing, a strictly normalized database is advantageous because writing operations can be carried out quickly and consistency guaranteed. At the same time, however, this brings with it massive reading problems, because a highly normalized database cannot be read out easily. Instead, it usually requires the use of numerous
JOIN statements, which slow down reading dramatically.
On the other hand, if the database is optimized for the most efficient reading, a completely denormalized system should be aimed for. In this case, a single
SELECT statement is sufficient for each read access, but writing becomes extremely time-consuming. The scenario also makes it extremely difficult and error-prone to guarantee consistency.
If, on the other hand, CQRS is used and the two aspects are separated on an architectural level, the problems go away. Then it is possible to work with two databases, one of which is normalized and responsible for writing, the other one denormalized and responsible for reading. This way, both writing and reading processes can be done optimally and with the best possible performance.
Thesis 1: CQRS is suitable for systems in which the number of writing and reading accesses differs greatly.
In addition, the separate scaling of an application's read/write side enables the application to be scaled in a way that it can be optimally adapted to the load of the respective situation as required.
Thesis 2: CQRS is suitable for systems whose read and write sides should be scaled individually.
However, this procedure means that the two databases must be synchronized. This in turn raises the question of the guarantees under which this is done. In particular, if the separation of writing and reading actually takes place with the help of physically different databases, it becomes clear that distributed transactions are probably not a very suitable means.
Therefore, in CQRS-based systems, the guaranteed consistency between the read and write sides is often given up in favor of availability: In case of doubt, it is better to get a response from the system, even if it may be slightly outdated, than none at all.
Of course, this does not apply to all scenarios. It is obvious that the approach is not appropriate, for example, for systems that affect people's lives or health: guaranteed consistency is probably desirable in the case of an eye laser, surgical intervention or the control of a nuclear power plant.
However, many other cases do well with a soft consistency. Real life also works in many places with this so-called eventual consisteny, i.e. an occasional consistency: Whoever orders a drink in a café usually receives the goods before they have to be paid for. This means that there is no transaction, which is why consistency from the café's point of view is not guaranteed in the meantime.
Thesis 3: CQRS is suitable for systems where availability is more important than consistency and eventual consistency is not an exclusion criterion.
Considering the approach to be complete, this means that commands sent to the application do not return anything – completely in accordance with the CQS principle, which stipulates that commands change the state and have side-effects, but that they cannot return information about the internal state. But what do you do with the results of the commands that do necessarily exist?
Of course, the user interface can use a query to check regularly whether a result exists, but such a pull-based procedure is cumbersome and time-consuming. It would be better to have a push notification, which will be delivered automatically as soon as a command is processed. Exactly this is solved with the help of so-called events, which represent a reaction to a command.
Thesis 4: CQRS is suitable for systems that work with commands and (asynchronous) events to map the interaction with the user.
For the user interface, this means that a command is first sent away in a fire-and-forget style and then the UI waits for the associated event. It is questionable whether or not you want to prevent the user from performing other tasks during this time. If you allow the user to wait, this results in a consistent state of the UI, but his nerves are often unnecessarily strained.
Therefore, assuming that most of the commands are processed successfully anyway, you can let the UI work asynchronously: As soon as a command is delivered to the backend, only the receipt is acknowledged. The user can then continue working and even navigate to other parts of the application if necessary. The result of the command is then displayed asynchronously at a later time, if this is still relevant. This is often only relevant in the event of an error.
Thesis 5: CQRS is suitable for systems whose graphical user interface can or should work asynchronously.
Another option to quickly give feedback to the user is to falsify the application's response in the graphical user interface, i.e. display the probable response directly. This is the way most online shops work, for example, which initially confirm receipt of the order and claim that it is now being processed and delivered. In fact, processing often only starts at a later point in time, which the customer only learns in the event of an error, for example, if the desired article is no longer in stock.
Although events are not the original concept of CQRS, they are an excellent counterpart to commands. Therefore, it is advisable to collect these events in a database and use them as a starting point for changing the status. The principle is called event sourcing.
Thesis 6: CQRS is suitable for systems with a persistence layer based on event sourcing.
This does not store the current state of the application, but the individual events that have led to the current state. The current status can then be restored at any later point in time via a replay. A database that stores such events and is optimized for the execution of replays is called event store.
The read database can also be filled from these events by semantically interpreting the individual events and mapping them to classic CRUD statements. Since the events contain domain semantics, they can be interpreted differently as required, so that different read tables can be generated from the same raw data.
Since the events do not describe the current status, but the way to get there, this can be done afterwards, for example, to answer questions that have arisen only in the course of time: Provided that the semantics contained in the events permit the corresponding evaluation, this is possible without any problems.
In addition, CQRS can also be perfectly combined with DDD (domain-driven design) as the command- and event-oriented approach fits in well with the concept that puts domain-oriented events at the forefront of software modeling. Of course, CQRS can also be used without event sourcing or DDD, just as these concepts work without CQRS. However, there is no denying that the three concepts complement each other very well.
Thesis 7: CQRS is suitable for systems that use DDD to model the underlying domain.
What about CRUD?
Occasionally, CQRS is also mentioned in connection with CRUD, but usually not as a suitable supplement, but as a contrast. Theoretically, the two approaches do not exclude each other, but in practice there is hardly any benefit from their combination: Since CQRS requires the separation of writing and reading, one acts with two databases or at least with two database schemas, which have to be synchronized as already mentioned.
This is extremely difficult with pure CRUD, as with CRUD there is no semantics for updating the read side. As described above, these can be obtained via domain events, which can then be used both as feedback to the user interface and as data for the event store.
Nevertheless, there are numerous applications where pure CRUD is completely legitimate. This is the case, for example, if an application ultimately only does forms over data, i.e. does not contain any complex domain logic, but merely provides masks with which the raw data from the database can be edited.
Thesis 8: CQRS is suitable for systems whose domain logic is too complex for pure CRUD.
CQRS is an exciting architectural approach that demands an unusual handling of data. The separation of writing and reading might be familiar to the fewest developers, but makes sense in terms of scalability, modern asynchronous user interfaces, and the proximity to event sourcing and DDD.
Nevertheless, CQRS is not the magic silver bullet that solves all problems. CQRS is particularly not suitable for small applications that do not require a high degree of scalability and that do not have complex domain logic, and for applications that have a direct impact on life or health, CQRS is not or only to a very limited extent suitable. Other approaches may be preferable here.
However, CQRS is ideal for most web and cloud applications: here, scalability is often an essential requirement for the software. In addition, much more is usually read than written, which speaks for the individual scalability of both sides. If you add event sourcing and DDD to CQRS, you have an excellent basis for the development of modern web and cloud applications.