Domain-Driven Design: The Power of CQRS and Event Sourcing
How CQRS/ES Redefine Building Scalable Systems
Over the past 15 years, I’ve been deep diving into Domain-Driven Design (DDD), learning and improving every step of the way. I’ve always been curious, especially when simple client requests turn out to be way more complex once you dig a little deeper. I’m particularly thankful for tools like EventStorming that help uncover the real processes and events behind what seems like straightforward requirements, making everything clear not just to me but also to clients and domain experts.
About 10 years ago, I first used Command Query Responsibility Segregation (CQRS) and Event Sourcing (ES) in a business application. This approach opened my eyes to many benefits, even though it definitely came with its challenges. But, one of the biggest game changers for me was realizing how CQRS naturally makes data flow in one direction. This single direction flow isn’t just about keeping things organized; it’s a game changer for how we build, maintain, and scale systems. It means we can separate the actions that change data from the actions that read data. This doesn’t just clear up a lot of potential mess; it also gives us a straightforward way to handle complex systems.
While I’m a big fan of using CQRS/ES in my projects, it’s not the only way to successfully apply DDD. DDD is a big field with lots of tools and approaches, and the best one really depends on what you’re working on, and also the skill level of your team. CQRS has been a great fit for me, especially when dealing with complex systems where separating the read and write operations can make everything more manageable. Every project is unique, and there are plenty of situations where other strategies might be the better choice.
The Benefits of CQRS
But what is CQRS? It’s is a powerful concept. At its core, it involves separating the tasks of writing data (commands) and reading data (queries) into two distinct parts. This separation is intended to clarify the structure of the system, delineate responsibilities, and increase the speed at which data can be read from the system.
I’ve noticed that CQRS is often seen as a performance optimization strategy due to the strict separation of read and write storage. While boosting performance is indeed a significant advantage, it barely scratches the surface of the benefits.
CQRS offers much more. First off, this approach gives us a clean, organized structure. This organization extends into a fantastic vertical slice of domain concerns, meaning we can focus on specific areas without getting lost in a sea of complexity.
Another huge plus is the enforced unidirectional flow of data. This isn’t just about keeping data movement tidy; it fundamentally changes how we design and think about our systems. It ensures that our data moves in a predictable pattern, reducing confusion and errors.
CQRS also pushes us to focus more on behavior rather than just data. This means we’re designing our systems with real-world actions and consequences in mind, steering clear of anemic domain models that are all too common in software projects. An anemic domain model is like a skeleton without muscles – it might have all the parts, but it can’t do much on its own. CQRS helps us avoid that, ensuring our models are rich with behavior and functionality.
The separation of concerns in CQRS allows us to enhance system views easily. Since read models are separate, we can tweak and improve them without touching the core logic of our application. This flexibility is invaluable for evolving systems to meet new needs or address new insights without a major overhaul.
Mirrored Databases are not CQRS
I’ve seen a trend where the read/write split in CQRS is interpreted as having two mirrored databases with identical data models, relying on replication for synchronization. However, this approach may miss the point of what CQRS is really about.
The heart of the matter is the high coupling that comes from using the same data model for both writes and reads. CQRS shines when it allows independence, allowing the write side to develop a rich, behavior-driven model while the read side optimizes for queries. Mirroring data models across databases ties the two sides too tightly together, limiting the flexibility of the system and the potential for each side to evolve as needed.
In addition, this setup runs the risk of turning the write model into a thin layer of logic around a database schema, rather than a robust domain model. It keeps us stuck in a data-centric mindset where the business logic isn’t where it should be — embedded in the domain itself.
It is important to understand that the write model is very different from the read model. Otherwise, all flexibility and power is lost.
Behavior Over Data with CQRS/ES
For me, an important benefit of combining CQRS with Event Sourcing is that it shifts the focus to behavior rather than just data models, so to speak by design. This perspective is essential because it aligns our development efforts much more closely with the real-world processes and logic that our software is intended to represent. Traditional development practices often place too much emphasis on the structure of data — how it’s stored, retrieved, and modified. While this approach is important, it can also lead us to overlook the actual behaviors and interactions that generate and consume this data. And believe me, I’ve seen this happen more than once. CQRS, when coupled with ES, reverses this focus. It encourages us to model our systems around the business events and actions that drive change, not just the data that those changes affect.
This means that our code reflects business logic in a much more expressive way. Instead of simply inserting, updating, or deleting rows in a table, we capture meaningful business events such as “order placed”, “user created”, or “inventory updated”. This makes our models richer and more meaningful. It improves our ability to rethink and evolve our systems over time.
The Power Of Projections
One of the most important aspects of the CQRS pattern is the concept of projections, which essentially serve as the basis for read models. This approach has profound implications for the way we deal with data in our applications because, most importantly, it frees us from the constraints associated with representing data on the write side. This separation ensures that the focus remains on accurately capturing behaviors and domain events, without the added complexity of how that data should be queried or displayed.
Read-side independence provides the flexibility to tailor data projections to a wide range of needs. Whether it’s structuring data for efficient querying or optimizing for specific views, projections can be designed to meet your needs. This capability is about meeting today’s needs and can also be easily adapted and extended to meet future requirements without touching existing code.
For projections, there is no need to revise existing structures or change the logic on the write side. Instead, we can simply create a new, isolated module specifically designed to project and analyze data as needed. This modularity and extensibility is a huge advantage, allowing our systems to evolve organically without compromising the integrity of the core domain logic.
Closing Thoughts
CQRS combined with Event Sourcing changes the way we think about and build our software systems. By separating the writing and reading of data, we get a clean, organized structure that improves performance and scalability. More importantly, this approach shifts our focus to behavior and real-world actions, ensuring that our models are rich in functionality and aligned with the domain they represent. Projections also allow us to adapt and extend our systems without touching existing code, underscoring the flexibility and robustness of CQRS/ES.
Cheers!