Hello! Let's begin today's lesson.
Introduction
In our previous lesson, we established a solid structural foundation for our services using Clean Architecture. We learned how to organize code into layers—Entities, Use Cases, Interface Adapters, and Frameworks—governed by the strict Dependency Rule, ensuring our core business logic remains isolated from external details. This structure makes our system testable, maintainable, and adaptable.
Today, we will build directly on that foundation to explore a powerful architectural pattern that is particularly relevant for the high-load systems you specialize in. The learning outcome for this lesson is to: Design a CQRS-based architecture for a given workload, implementing separate read and write models.
CQRS, or Command Query Responsibility Segregation, takes the separation of concerns principle a step further. It advocates for creating two distinct models and processing paths within your application: one for changing state (Commands) and another for reading state (Queries).
For many complex, high-throughput systems, the demands of reading data are vastly different from the demands of writing it. CQRS allows us to optimize for each path independently. We will cover:
- The core concepts, benefits, and significant trade-offs of CQRS.
- How to implement CQRS logically with separate models but a single database.
- The practical details of creating an optimized read model and a transaction-focused write model.
This pattern directly addresses many challenges in distributed systems, such as performance tuning for disparate workloads and managing domain complexity.
1. The "What" and "Why" of CQRS
Before diving into implementation, it's crucial to understand the conceptual underpinnings of CQRS and, just as importantly, when not to use it. The pattern introduces complexity, and its benefits are most pronounced in specific contexts.
To get the most authoritative and balanced perspective, let's start with the article by Martin Fowler, who helped popularize the pattern.
This article, 'CQRS' by Martin Fowler, provides the essential high-level overview. It defines the pattern, contrasts it with the traditional CRUD approach, and discusses its relationship with other patterns like DDD and Event Sourcing.
Please read the first three sections of the article: 'What is CQRS?', 'CQRS and Related Architectural Patterns', and 'When to use it'. Pay close attention to: The fundamental split between the Command model (for updates) and the Query model (for display). The distinction between using shared vs. separate databases for the two models. The strong cautions about the added complexity and the advice to apply it only to specific, complex parts of a system (Bounded Contexts) rather than wholesale.
To complement Fowler's conceptual overview, the Microsoft Azure Architecture Center provides a more structured breakdown with helpful diagrams.
CQRS Pattern - Azure Architecture Center
This guide from the Azure Architecture Center, 'CQRS Pattern,' offers a structured look at the problems CQRS solves and its implementation strategies.
Please review this document, focusing on the diagrams and the lists of benefits and challenges. Specifically, look at the diagrams under 'Separate models in a single data store' and 'Separate models in different data stores'. These visuals clearly illustrate the two main architectural approaches.
Key Concepts Summarized
From these resources, we can distill the core ideas:
- Segregation: At its heart, CQRS separates the responsibility of handling commands (operations that change state) from queries (operations that read state).
- Different Models: This separation isn't just about different methods; it's about using entirely different models.
- The Write Model (or Command Model) is concerned with processing commands, enforcing business rules, and ensuring transactional consistency. This is where your rich DDD Aggregates, which we discussed previously, would live.
- The Read Model (or Query Model) is optimized purely for display. It often consists of denormalized data tailored to specific UI components, avoiding complex joins and calculations at query time.
- Architectural Variants:
- Logical Separation: The read and write models can use different object models and data access logic but share the same physical database.
- Physical Separation: A more advanced form uses a separate database for the read model, which is often a denormalized, highly optimized projection of the write database. This introduces eventual consistency.

The primary driver for adopting CQRS is often a significant mismatch between read and write requirements, a common scenario in the high-load payment and trading systems you've built. For example, a trading system might have a very high volume of read-only requests for market data but a much lower, yet more complex and critical, volume of trade execution commands.
2. Implementing Separate Read and Write Models
The most pragmatic way to start with CQRS is by implementing a logical separation within a single service, using a single database. This approach gives you many of the benefits—model optimization, separation of concerns—without the added operational complexity of managing data synchronization between two different databases.
This is where our previous work on Clean Architecture pays off. The Use Cases (or Application) layer is the perfect place to create this split.
The following video demonstrates how to set up the foundational messaging abstractions for commands and queries within a Clean Architecture project.
How to Implement the CQRS Pattern in Clean Architecture (from scratch)
In this video, 'How to Implement the CQRS Pattern in Clean Architecture,' Milan Jovanović lays the groundwork by defining interfaces for commands and queries. This establishes a clear contract for the two sides of the application.
Please watch these segments to understand how the application is logically divided: Introduction to CQRS (00:00 - 01:59): A quick recap of the core idea. Logical CQRS with a Single Database (01:47 - 03:17): This part visualizes the architecture we are about to build: a simple, fast path for queries and a more complex, domain-driven path for commands. Defining Command & Query Abstractions (03:56 - 08:58): Observe how interfaces like ICommand, ICommandHandler, IQuery, and IQueryHandler are created. This is a standard pattern for implementing CQRS, often facilitated by libraries like MediatR in .NET or Spring CQS in Java.
Now for the central part of today's lesson: creating the separate models. We'll have one model for writing—our rich DDD aggregate—and a completely different, flat model for reading.
This next video is a masterclass in how to achieve this using two different data contexts that point to the same underlying database. While the example uses .NET's Entity Framework, the principles are directly applicable to Java's JPA (EntityManager) or using a combination of JPA for writes and a more direct tool like jOOQ or Spring's JdbcTemplate for reads.
Using Separate Read/Write Models with EF Core and CQRS
This video, 'Using Separate Read/Write Models with EF Core and CQRS,' demonstrates the core technique for implementing logical CQRS. It shows how to tailor each model for its specific purpose.
Please watch these sections carefully. This is the most practical part of our lesson. Motivation (00:00 - 01:22): Explains why a rich domain model is great for writes but cumbersome for queries. Configuring Write & Read Contexts (01:22 - 06:40): See how two distinct DbContexts are created. The Write context is configured for the domain entities. The Read context is configured with a crucial optimization: QueryTrackingBehavior.NoTracking. This tells the ORM not to track changes, significantly speeding up read-only operations. Defining Read Models (06:29 - 08:39): Observe the creation of UserReadModel. It's a simple class with primitive types and public setters, designed to be a data container (DTO). Notice how it can even include navigation properties to simplify joins on the read side, something often discouraged in rich domain models.
This approach is powerful because it allows you to have the best of both worlds:
- On the write path, you use your
ApplicationWriteDbContextwith the fullUseraggregate. You can enforce invariants, encapsulate logic in value objects, and perform complex, consistent transactions. - On the read path, you use the
ApplicationReadDbContextwith the simpleUserReadModel. Queries become straightforward projections to DTOs. You bypass the overhead of change tracking and can construct queries that are far more performant because they are tailored exactly to what the UI needs.
3. A Tale of Two Paths: Command vs. Query
Let's see how these two distinct paths behave in practice by looking at the implementation of a command handler versus a query handler.
The Command Path: Logic and Consistency
The command path is where your business logic lives. It's transactional, stateful, and complex.
How to Implement the CQRS Pattern in Clean Architecture (from scratch)
Let's return to the first video to see a complete command-handling flow. This will tie together our lessons on DDD, Clean Architecture, and CQRS.
Watch the 'Start Following' Command example (08:58 - 15:45). Note the sequence of operations: The StartFollowingCommandHandler is instantiated. It uses a repository (IUserRepository) to fetch the rich User aggregates. It invokes a FollowerService (a domain service) to execute the core business logic. It uses a UnitOfWork to commit the transaction, persisting the changes made to the aggregates.
This path is intentionally layered to ensure correctness and maintainability.
The Query Path: Speed and Simplicity
The query path, by contrast, should be as simple and direct as possible. Its only job is to fetch data for display.
Using Separate Read/Write Models with EF Core and CQRS
Now, let's look at how a query is transformed to use the new, optimized read model.
Watch the 'Updating Queries to Use Read Model' segment (08:39 - 10:50). Notice how the query handler now uses the ApplicationReadDbContext. The query is a simple LINQ projection directly to a DTO, using the UserReadModel. It's a straight shot from the database to the client with minimal overhead.
The difference is stark. The query handler has no business logic, no aggregates, and no unit of work. It is a thin, efficient data retrieval mechanism.
Conclusion
Today we've dissected the CQRS pattern, moving from high-level theory to a concrete, practical implementation strategy that builds upon our existing architectural principles.
Key Takeaways:
- CQRS segregates command (write) and query (read) operations, allowing for independent optimization of each path.
- It is best suited for complex domains or high-performance applications with asymmetric read/write workloads, and should be applied selectively to specific bounded contexts.
- A pragmatic first step is logical CQRS, using separate object models for reads and writes but sharing a single database.
- The write model should be your rich domain model (aggregates) focused on transactional consistency and enforcing business rules.
- The read model should be a set of simple, denormalized DTOs optimized for fast, read-only queries, bypassing unnecessary ORM features like change tracking.
- This separation results in a clear architectural divide: a complex, logic-heavy path for commands and a simple, fast path for queries.
Preview of the Next Lesson
CQRS is often mentioned in the same breath as another powerful pattern: Event Sourcing. While CQRS separates reads from writes, Event Sourcing changes the fundamental way we persist state on the write side.
In our next lesson, "Explain Event Sourcing concepts: event stream, aggregates, projections, and snapshots," we will explore how we can stop storing the current state of our aggregates and instead store the full history of events that have happened to them. This approach pairs naturally with CQRS, where the event store becomes the definitive write model, and the read models are built as projections from this stream of events.