This article is a translation of the material Tackling Complexity in CQRS.
The CQRS pattern can work wonders: it can maximize scalability, performance, security, and even ‘Beat’ the CAP theorem… However, for example, in his article on CQRS Martin Fowler argues that the pattern should be applied sparingly and even carefully:
“… for most systems, CQRS adds risks”
“… you have to be very careful when using CQRS”
“So while CQRS is a good template to have in a toolbox, keep in mind that it is difficult to apply correctly and you can easily miss important parts if you misuse it.”
From my point of view, the complexity caused by CQRS is largely random and therefore avoidable. To illustrate my point, I want to discuss the purpose of CQRS and then analyze 3 common sources of random complexity in systems using CQRS.
Purpose of CQRS
The purpose of CQRS is to provide presentation of the same data using multiple models. Neither scalability, nor availability, nor security, nor performance. The same data across multiple models. That’s all. The rest are by-products. Don’t believe me? Listen to the performance Greg Young at DDDEU2016where he says that CQRS was invented to support the Event Sourcing implementation. And as you probably know, the Event Sourcing model is good for writing data, but terrible for reading. That’s why he needed CQRS then: to represent the same data in multiple models.
How does CQRS achieve this goal? Making sure that only one model serves as the source of truth, and all changes apply only to that model. Let’s see how this understanding can help us deal with some of the complexities.
Trap # 1: One-sided Commands or Over-Splitting
All CQRS definitions I know follow the following pattern:
CQRS is based on the CQS principle, which states that operations should be divided into two groups: commands that modify data and queries that return data. Once we elevate this principle to the architectural level, we have a system with use cases, divided into the same two groups: commands and requests. Each use case can be a command or a query, but not both.
Once the use cases are decoupled, we get quite a few benefits: multiple models, different persistence mechanisms, independent scalability, etc.
Do you feel that something is wrong here? The problem is subtle: all definitions of CQRS usually start with a solution – separation, and only then define the problem – several models. This causes too much zeal for sharing: it gets to the point of defining commands as one-way when you get an Ack / Nack response from the server, but you need to poll some read model store for the actual result of the command. In other words, great difficulties have appeared.
Solution: weaken the division
Let’s take a step back and revisit the separation. We have seen that, according to CQRS, to represent the same data in multiple models, a use case can either write or read data. Obviously, the reading model shouldn’t update anything, or we’ll end up with multiple sources of truth. But should you really leave your commands not returning an execution result?
Not really. Without violating any principles, the command can safely return the following data:
Execution result – success / failure;
Error messages or validation errors in case of failure;
Unit new version number, if successful;
This information will greatly improve the user experience of your system because:
You do not need to interrogate an external source to get the result of the command, you have it right away. It becomes trivial to check commands and return error messages; and
if you want to update the displayed data, you can use the new version of the aggregate to determine if the view model reflects the command that was executed or not. You no longer need to display stale data.
Speaking of data, can we ease the separation a bit? In many cases, any data contained within the affected aggregate can be returned as part of the command’s output. However, there is a small caveat here: make sure that the returned data can be requested later from the reader model. Otherwise, there is a small risk of data loss if the response does not reach the client.
You can see an example of this approach in Daniel Whittaker’s blogwhere he discusses using command execution objects to test them. Moreover, in this example you can see the command result object that I am using in C #.
Trap # 2: Event sourcing
For historical reasons, CQRS is closely related to the Event Sourcing pattern. After all, CQRS was invented to make Event Sourcing possible. But let’s reevaluate the connection between the two patterns.
As I said earlier, the purpose of CQRS is to provide the same data in different models. If you are working with the Event Sourced Domain Model, you absolutely need CQRS to be able to execute queries. However, there are many other very good reasons for implementing CQRS that are not related to Event Sourcing:
Your system should display its entities in different view models.
You must support different query models (search, graphs, documents, etc.).
The difference between writing and reading is very different and you want to scale them independently.
You hate ORM.
Does this mean that in all these cases, you need Event Sourcing? If you do this, you will find yourself trapped in complexity. Event Sourcing is a way to model a business domain. Not just a way, but probably the most difficult way. Therefore, you should use Event Sourcing if and only if your business domain warrants it. Let’s see how you can implement CQRS in other cases.
Solution: CQRS! = Event sourcing
We were taught to create projections by writing event handlers. But how do you implement projection without events? There is another way, and I call it State Based Projections. This topic deserves a separate post, but I will briefly describe three ways to implement “state-based projection”:
Method One: Flag “Is Dirty”
You can mark an object that has been updated with the IsDirty flag (setting it to true) and implement a projection mechanism that will query dirty instances and project updated data into separate models.
Method two: catch-up
In relational databases, you can track commits at the table level. For example, SQL Server has a built-in mechanism for this – the “row version” column. Similar functionality can be implemented for other relational databases. The projection engine will query for updated rows in a subscription-like manner and project the updated data.
Third way: DB views
If you are using a relational database and all you need to do is present its data in different models, the DB views will work just fine. Yes, a perfectly valid CQRS system can be implemented in the database. Perhaps the least attractive solution, but not only does it work, it naturally follows the CQRS pattern.
These ways of projecting models may not be very cool, but they work. I’ve seen quite a few projects where they have been involved and they have worked great without unduly diving into the complexities of Event Sourcing.
Wait, I was suggesting to ignore Event Sourcing at all costs because it is difficult? Of course not! Event Sourcing is one of the most important tools. But like any other tool, use it in your context – the business domains that add value to the business: the core subdomain. On the other hand, generic and supporting subdomains that are easy enough to implement using Transaction Script or Active Record templates can still benefit from CQRS. If so, use the simplest tool that does the job and benefit from CQRS with State Based Projections.
Trap # 3: Too Much Good
The hype around microservices has drawn a lot of attention to CQRS: if you have a set of independent services that need to query each other for data, CQRS is a generic solution. However, I’ve seen this approach create monstrous data flow diagrams of services projecting large amounts of data among themselves.
This is not always a bad thing, but in many cases it can be a signal to take a step back and rethink your decomposition strategy. Chances are, your services are too granular and do not reflect the boundaries of the business domain. In this case, you can greatly simplify your architecture by reconfiguring service boundaries with appropriate business domains.
I want to summarize it all with a CQRS schema:
This chart is different from other charts you can find on the Internet:
This is the CQRS pattern as I see it and implement it. The teams have answers. The projection engine is defined abstractly, without any implementation details. Internally, it can be event-based, or state-based, or even database-based. Finally, there is not even a hint of Event Sourcing. Model the business logic of the system according to the requirements of the business domain: Active Record, Domain Model, or Event Sourced Domain Model.
As with any tool used correctly, CQRS should reduce complexity, not cause it. If the complexity of your architecture grows, you are doing something wrong.