I published an online course about CQRS a couple months ago, and since then I realized that there are some topics I didn’t put enough emphasize on in that course, or didn’t cover at all. In the next several blog posts, I’m going to fill this gap.
This article is about CQRS commands and whether they are part of the domain model.
CQRS commands and the onion architecture
So, are CQRS commands part of the domain model? They are. Here’s the onion architecture diagram I’ve been using for several years now:
It’s a bit simplified: there are only three layers. I also added a separation between pure and impure domain services, i.e. those services which refer to the external world such as a database, the file system, and those which don’t.
Commands belong to the core domain (just like domain events). They play an important role in the CQRS architecture – they explicitly represent what the clients can do with the application. Just like events represent what the outcome of those actions could be.
Commands and events are two ends of the same stick, they reside at the same abstraction level. Commands are what triggers a reaction in the domain model. And events are the result of that reaction.
The only difference between them is that the commands follow the push model while the events – the pull one. The push model means that it’s someone else, not your application, that raises the commands. The pull model is the opposite of that – it’s your application that is responsible for raising the events.
By the way, queries are not on these diagrams because they don’t belong to the onion architecture. Domain modeling is for writes, not reads. Reading data is simple, you don’t need DDD to do that. But you do need a rich and highly encapsulated domain model for data modification. It has a potential for data corruption that you need to have a good protection from.
Commands vs DTOs
A lot of people are hesitant to treat commands as part of the core domain, though. And that’s understandable given how commands are often used:
In the above example, the command serves as a data container for the incoming request. Which is clearly not what you want your domain classes to be used as. Data that comes from external applications should be represented by a special type of classes – Data Transfer Objects (DTOs for short). I wrote about DTOs in detail in this article: DTO vs Value Object vs POCO.
Here’s how the controller method should look instead:
This version contains an explicit mapping between the incoming data (represented by the DTO) and the command. You shouldn’t skip this mapping stage. Otherwise, it would indeed be problematic to treat commands as part of the core domain model.
Why? Because commands and DTOs are different things, they tackle different problems. Commands are serializable method calls – calls of the methods in the domain model. Whereas DTOs are the data contracts. The main reason to introduce this separate layer with data contracts is to provide backward compatibility for the clients of your API.
Without the DTOs, the API will have breaking changes with every modification of the domain model. Or you won’t be able to properly evolve the domain model because of the constraints imposed by the backward compatibility. Either way, using the commands in place of DTOs forces you to choose one of the two sub-optimal design decisions.
On the other hand, the DTOs and the mapping between them and the commands ensure that your application will be both backward compatible and easy to refactor.
Using commands in place of DTOs is akin to using domain entities for the same purpose, which is extremely harmful for the encapsulation of your application. The use of commands is not as bad of course, because the commands themselves don’t contain any business logic, but the drawbacks are similar – the use of entities or commands in place of DTOs hinders your ability to refactor the domain model.
Let’s say for example that you decide to split the student name into first and last names:
If you use the commands to store the data of incoming requests, you won’t be able to modify the command itself. This would violate the backward compatibility. You need to keep two versions of the command and be able to process both.
It’s much cleaner and more maintainable to just have two layers: the DTOs and the commands, each playing their own part. DTOs for backward compatibility where you can have as many versions of the data contracts as you want, and commands for the explicit representation of what the clients can do with the application. It’s much easier to implement the mapping between the two than to try to lump all these responsibilities into the commands.
Having all that said, it’s fine to use commands in place of DTOs if you don’t need backward compatibility in your application. For example, if both the UI and the backend are hosted in the same process (see: a desktop application), or you develop the API and the clients of that API yourself (see: most enterprise-level web applications), then it means you can deploy both at the same time. Therefore, breaking changes are not an issue since all clients and the API itself will have the latest version deployed simultaneously, you won’t end up in a situation where you have an old version of the client talking to the new version of the API.
But keep in mind that it’s an edge case. Treat it as a code complexity optimization technique. In a case of publicly available API, or if you cannot deploy the client and the API simultaneously, you do need both the DTOs and the commands.
Alright, that was a rather long version of “Yes” to the question of “Are CQRS commands part of the domain model?”. Let’s summarize:
- Commands are part of the core domain model. Just like events
- Commands represent what the clients can do with the application. Just like events represent what the outcome of those actions could be
- Commands are what triggers a reaction in the domain model. Events are the result of that reaction
- Commands follow the push model. Events – the pull one
- Have both commands and DTOs by default
- View commands as serializable method calls
- View DTOs as data contracts that help you achieve backward compatibility
- You can use commands in place of DTOs if you don’t need backward compatibility