I completed the course and I sincerely recommend it to anybody that wants to learn more about the Command Query Responsibility Segregation (CQRS) pattern and Domain-Driven Design (DDD) in general.
The course does not cover Event Sourcing (ES), what I like because allows learning CQRS in-depth avoiding getting confused with the most advanced CQRS+ES or getting advice that only applies when ES is been used.
That said, I would love if Vladimir can expand his DDD course series with a Event Sourcing course ;-).
Vladimir introduces all the concepts in a good progression, by layers (API, Domain, Database) and complexity, reminding us that there is no need to apply a full CQRS pattern to start getting benefits from it.
My notes:
Module #1 – Introduction
– With CQRS instead of having one unified model we have two: one for reads and another for writes.
Module #3 – Refactoring Towards a Task-based Interface
– Task-based Interface vs CRUD-based Interface.
– I found really good and realistic the example Vladimir uses, a legacy system exposing a single update endpoint is used by the UI for diverse uses cases (edit personal info, enrolling/disenrolling, transferring student), but nothing is explicit in the API side. What is there is a huge update method doing multiple things. The video shows how to split and refactor it away from a CRUD API to a task-based interface.
– Task-based interface involves not only the API also the UI.
Module #4 – Segregating Commands and Queries
– The terms Command and Query are overloaded, they mean command or query method in the context of CQS but just a data class in the context of CQRS.
– The objects representing our Commands and Queries belong to the Domain Model, as the Domain Events do (all 3 are “messages”). What does not belong to the domain layer are the Handlers that are part of the Application Services layer.
– Vladimir depicts an Onion Architecture with 3 layers: Core domain (Entities, Aggregates, Value Objects, Domain Events, Commands, Queries, Pure Domain Services), Non-core domain (Repositories, Impure Domain Services), Non-domain (Application Services, Command Handlers, UI).
– Commands follow the push model whereas Events follow the pull model.
– Commands and Data Transfer Objects (DTOs) are different things, they tackle different problems. Think in Commands as serializable method calls (calls in the domain model) whereas DTOs are data contracts between the API and its clients. This separation layer allows us to refactor or evolve the domain model without introducing breaking changes in the API (backward compatibility). We are used to doing this in our mapping between Entities and DTOs.
– Vladimir makes a really good point explaining that the reason why people often don’t see commands as part of the domain model is that in the controllers they skip the mapping stage and directly use commands for the request/response body. Don’t do that.
// Don't do this [HttpPut("{id}")] public IActionResult EditPersonalInfo(long id, [FromBody] EditPersonalInfoCommand command) { Result result = _messages.Dispatch(command); return FromResult(result); } // Use DTOs [HttpPut("{id}")] public IActionResult EditPersonalInfo(long id, [FromBody] StudentPersonalInfoDto dto) { var command = new EditPersonalInfoCommand(id, dto.Name, dto.Email); Result result = _messages.Dispatch(command); return FromResult(result); }
– It is fine to not use DTOs for commands if you don’t need backward compatibility (when API and client can be deployed simultaneously).
– In the example, in order to make accessible the output DTO classes to the Query Handlers, Vladimir moves all DTOs from the API project to the Logic one but points that ideally they should reside in their own assembly.
– Once the refactoring to introduce explicit commands and queries is completed the Controller takes only care of the wiring, the application logic is now in the Handlers.
– I like the idea of always returning something from the Handlers, a more functional approach, Vladimir uses its own Result abstractions to indicate success or failure.
public interface ICommandHandler where TCommand : ICommand { Result Handle(TCommand command); } public interface IQueryHandler where TQuery : IQuery { TResult Handle(TQuery query); }
Module #5 – Implementing Decorators upon Command and Query Handlers
– Because we have Handlers in place with a consistent interface, we could decorate them to tackle cross-cutting concerns without code duplication and to follow the SRP. Multiple decorators can be chained.
– The code examples show how to create decorators for audit logging and also for database retries. The audit logging example is really simple and clean, taking advantage of the explicit command objects to serialize and log them.
– To keep organized commands, queries and handlers Vladimir recommends placing each Handler in the same file that its corresponding Command or Query as an internal class.
https://github.com/vkhorikov/CqrsInPractice/blob/ce5be7233a83b8dec1bcb3461d9ebf58d2aaf1de/After/src/Logic/AppServices/EditPersonalInfoCommand.cs#L23
– Don’t reuse command handlers. This is not to call a command handler from another command handler neither dispatch a command from a handler. Tackle duplication placing the reusable code in the domain model for example in a domain service.
– Our system should not create commands on its own, the clients (users or another system) are the ones originating commands and our system reacts to them. A command is a representation of what the client can do with our application.
– Vladimir arguments in favour of using hand-written decorators over ASP.NET middleware. Use middleware for ASP.NET related functionality and our own decorators for everything else (this way we have separation of the application and framework concerns).
Module #6 – Simplifying the Read Model
– Using the same domain model for reads and writes leads to two issues: a domain model overcomplicated, bad query performance (N+1 problem, some operations and filters are not supported by ORM and need to be done in memory, etc).
– The refactor to introduce the separation of the Domain Model is more about taking the domain model out of the read side than a split.
– The read side will not work with any domain model. The domain model is only required for commands, not for queries.
– Because there are no data modifications the read model does not need encapsulation, abstractions or an ORM. We can write database access calls directly. Database specific features can be used to optimize performance.
– Read model is a thin wrapper on top of the database.
– With the refactor the query method directly queries the DB retrieving all data needed with a single DB roundtrip what increases performance.
Module #7 – Introducing a Separate Database for Queries
– As a first step, we could have separate databases, by using the replication mechanism provided by the DB vendor and using the replicas to scale the read-side.
– No matter we improve performance on the read-side using direct queries or a separated replica, the underlying database schema in place will still be designed to support the needs of the write-side.
– We can do better if we have a denormalized schema designed to match the needs of the read-side. No need then to assemble data from different tables with complicated queries. Other option would be to use a different type of database as a document database.
Module #8 – Synchronizing the Commands and Queries Databases
– Synchronization = Projection
– The implementation of projections can follow two strategies: State-driven projections (sync or async), Event-driven projections.
– State-driven Projection can be implemented using a flag like “isSyncRequired” in each aggregate table or/and using a separated synchronization table. A separated sync process will check if there are records to sync with the read model.
– One way of implementing the command part can be with a database trigger that monitors all changes.
– A better way of implementing it can be introducing explicit flags in the domain model, this is in the entity and the use of event listeners. This reminds me of the way I usually publish domain events from aggregates (https://paucls.wordpress.com/2018/05/31/ddd-aggregate-roots-and-domain-events-publication/).
– In any case, we will need to implement soft deletion.
– In a Synchronous version, the application does the projection and all changes are consistent. The downside is increasing the processing time. This is not recommended at least when we have the separated databases, it does not scale.
– In Event-driven projections, the Domain Events drive the changes. The projection process will subscribe to the domain events raised by the command-side, which contains all information necessary, no need for checking synchronization flags/table to build the queries database.
– This scales very well, we can use a message bus. The problem is we cannot rebuild the read database in case of error because we do not store domain events (for this we will need Event Sourcing).
– If we are not using Event Sourcing Vladimir recommends using state-driven projections. This is aligning the projection strategy with the persistence mechanism.
– Commands database is always immediately consistent.
– Do not run a Query from a Command Handler, that would query the query database that might not be up to date. Query the command database (using a repository). An example can be a command that needs to validate that a username is not already taken.
– An Event Sourced system, for efficiency reason, has to query the read database.
– If we go for a separate database for reads then we will have Eventual Consistency. Train users not to expect data to be immediately consistent (the real world is asynchronous and eventually consistent). UX should provide helpful messages and expectations. It will help to keep a connection for example via WS.
– Introduce versioning to mitigate the problems of Eventual Consistency. Make the version number part of all the communications, this way the Commands can validate that the UI is operating with the current version of the aggregate, if not reject change and inform UI.
– The CAP theorem tells us that a distributed database system can only have 2 of the 3 properties: Consistency, Availability and Partitionability. Finding a proper balance is hard. CQRS allows choosing different properties for the read and the write sides. For example for the write-side full consistency and availability giving up partition tolerance (typical relational DB). On the read-side give up full consistency to have partitionability because scalability is important.
Module #9 – CQRS Best Practices and Misconceptions
– A common misconception is that Commands should not return anything, follow CQS principle, poll another endpoint for the result. Truly one-way commands are impossible, they should return some kind of acknowledgement (ok, error, or locator to pull result). If the operation can complete synchronously there is no need to return a locator. They also can return aggregate id and version number.
– The CQRS pattern and Specification pattern contradict each other.
https://enterprisecraftsmanship.com/2018/11/06/cqrs-vs-specification-pattern/
I if you are looking for more resources to learn CQRS I would also recommend this CQRS Tutorial.
A very good and clear article. It was very useful for me. Thanks for share 🙂