Types of CQRS

By Vladimir Khorikov

CQRS is a pretty defined concept. Often, people say that you either follow CQRS or not, meaning that it is some kind of a binary choice. In this article, I’d like to show that there is some wriggle room in this notion and how different types of CQRS can look like.

Type 0: no CQRS

With this type, you don’t have any CQRS whatsoever. That means you have a domain model and you use your domain classes for both serving commands and executing queries.

Let’s say you have a Customer class:

public class Customer

{

    public int Id { get; private set; }

    public string Name { get; private set; }

    public IReadOnlyList<Order> Orders { get; private set; }

 

    public void AddOrder(Order order)

    {

        /* … */

    }

 

    /* Other methods */

}

With the type 0 of CQRS you end up with CustomerRepository class looking like this:

public class CustomerRepository

{

    public void Save(Customer customer) { /* … */ }

    public Customer GetById(int id) { /* … */ }

    public IReadOnlyList<Customer> Search(string name) { /* … */ }

}

Search method here is a query. It is used for fetching customers’ data from database and returning it to a client (a UI layer or a separate application accessing your server through some API). Note that this method returns a list of domain objects.

The advantage of such approach is obvious: it has no code overhead. In other words, you have a single model that you use for both commands and queries and don’t have to duplicate the code at all.

The disadvantage here is that this single model is not optimized for read operations. If you need to show a list of customers in UI, you usually don’t want to display their orders. Instead, you most likely prefer to show only a brief information such as id, name and the number of orders.

The use of a domain class for transferring customers’ data from the database to UI leads to loading all their orders into memory and thus introduces a heavy overhead because UI needs the order count field only, not the orders themselves.

This type of CQRS is good for small applications with little or no performance requirements. For other types of applications, we need to move further.

Type 1: separated class structure

With this type of CQRS, you have your class structure separated for read and write operations. That means you create a set of DTOs to transfer the data you fetch from the database.

The DTO for Customer can look like this:

public class CustomerDto

{

    public int Id { get; set; }

    public string Name { get; set; }

    public int OrderCount { get; set; }

}

The Search method now returns a list of DTOs instead of a list of domain objects:

public class CustomerRepository

{

    public void Save(Customer customer) { /* … */ }

    public Customer GetById(int id) { /* … */ }

    public IReadOnlyList<CustomerDto> Search(string name) { /* … */ }

}

The Search method can use either an ORM or plain ADO.NET to get the data needed. This should be determined by performance requirements in each particular case. There’s no need to fall back to ADO.NET if a method’s performance is good enough.

DTOs introduce some duplication as we need to come up with the same concept twice: once for commands in a form of a domain class and once more for queries in a form of a DTO. But at the same time, they allow us to create clean and explicit data structures that perfectly align with our needs for read operations as they only contain data clients need to display. And the more explicit we are with our code, the better.

I would say that this type of CQRS is sufficient for most of enterprise applications as it gives a pretty good balance between code complexity and performance. Also, with this approach, we have some flexibility in terms of what tool to use for queries. If the performance of a method is not critical, we can use ORM and save developers’ time; otherwise, we may fall back to ADO.NET (or some lightweight ORM like Dapper) and write complex and optimized queries on our own.

If we want to continue separating our read and write models, we need to move further.

Type 2: separated model

This type of CQRS proposes using separated models and sets of API for serving read and write requests.

Types of CQRS: type 2

Type 2 of CQRS

That means that, in addition to DTOs, we extract all the read logic out of our model. Repository now contains only methods that regard to commands:

public class CustomerRepository

{

    public void Save(Customer customer) { /* … */ }

    public Customer GetById(int id) { /* … */ }

}

And the search logic resides in a separate class:

public class SearchCustomerQueryHandler

{

    public IReadOnlyList<CustomerDto> Execute(SearchCustomerQuery query)

    {

        /* … */

    }

}

This approach introduces more overhead comparing to the previous one in terms of code required to handle the complexity, but it is a good solution if you have a heavy read workload.

In addition to the ability to write optimized queries, type 2 of CQRS allows us to easily wrap read portion of API with some caching mechanism or even move read API to another server and setup a load-balancer/failover cluster. It works great if you have a massive disparity between the workload of writes and reads in your system as it allows you to scale the read part of it drastically.

If you need even more performance of read operations, you need to move to type 3 of CQRS.

Type 3: separated storage

That is the type that considered to be the true CQRS by many. To scale read operations even further, we can create a separate data storage optimized specifically for queries we have in our system. Often, such storage might be a NoSQL database like MongoDB or a replica set with several instances of it:

Types of CQRS: type 3

Type 3 of CQRS

The synchronization goes in background mode and can take some time. Such data storages are considered to be eventually consistent.

A good example here could be indexing of customers’ data with Elastic Search. Often we don’t want to use full-text search capabilities built into SQL Server as they don’t scale much. Instead, we could use non-relational data storage optimized specifically for searching customers.

Along with the best scalability for read operations, this type of CQRS brings the highest overhead. Not only should we segregate our read and write model logically, i.e. use different classes and even assemblies for it, but we also need to introduce database-level separation.

Summary

There are different types of CQRS you can leverage in your software; there’s nothing wrong with sticking to the type #1 and not moving further to the types 2 or 3 as long as the type #1 meets your application’s requirements.

I’d like to emphasize this once more: CQRS is not a binary choice. There are some different variations between not separating reads and writes at all (type 0) and separating them completely (type 3).

There should be a balance between the degree of segregation and complexity overhead it introduces. The balance itself should be found in each concrete software application apart, often after several iterations. I strongly believe that CQRS itself should not be implemented “just because we can”; it should only be brought to the table to meet concrete requirements, namely, to scale read operations of the application.

LinkedInRedditTumblrBufferPocketShare




  • George110

    Lucid and concise description. There are a lot of ideas the implementation of CQRS and the purists want to stick to dogma at the expense of practicality. Your approach is pragmatic and workable in real life.

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Thanks, that is the exact kind of a message I was trying to convey.

  • gregyoung

    This post misses. Event sourcing.

    Also between 1 and 2:

    “This approach introduces more overhead comparing to the previous one in terms of code required to handle the complexity, but it is a good solution if you have a heavy read workload.”

    Its moving a method out of a repository to another object. It won’t help at all with your “read work load” nor does it really add or remove “code complexity” unless you consider having two types as opposed to one “code complexity”. Your type 1 just puts the same method on a repository returning a dto as opposed to on a query object returning a dto. The code in the method is the same.

    Also as the person who named/definedCQRS I would not consider type 1 to be following CQRS. http://codebetter.com/gregyoung/2010/02/16/cqrs-task-based-uis-event-sourcing-agh/ type 1 doesn’t actually do the main part of CQRS.

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      I’ve definitely broadened the definition of CQRS. And I’m not the only one who does this:

      Regarding 2 as opposed to 1: it doesn’t increase the workload by itself, but it enables you to wrap the read portion of the API with a caching or load-balancing mechanism – the thing you can’t do with 1.

      • gregyoung

        “Regarding 2 as opposed to 1: it doesn’t increase the workload by itself, but it enables you to wrap the read portion of the API with a caching or load-balancing mechanism – the thing you can’t do with 1.”

        You can do it with #1 as well. But either is just baking accidental complexity into the model.

        • http://enterprisecraftsmanship.com/ Vladimir Khorikov

          With #1, reads are not separated from writes on the API level, so the only thing you can do with 1 is wrap the whole API which doesn’t help much if you need to do this only for reads.

          • gregyoung

            “With #1, reads are not separated from writes on the API level, so the only thing you can do with 1 is wrap the whole API which doesn’t help much if you need to do this only for reads.”

            which by definition is not CQRS. You also didn’t specify this in your post (you only showed the inside of the domain model not the external contract).

            I was referring to the ability to cache in front of data source which you can do with either.

            But even on the first generally you cache GETs only not posts. Oddly commands and queries match POST/GET in http. You can still do it.

            #1 is not CQRS at all.

          • http://enterprisecraftsmanship.com/ Vladimir Khorikov

            #1 is not CQRS at all.

            I’m definitely not claiming my categorization is canonical, so you are right with this one. Nevertheless, I do think #1 serves the same purpose as canonical CQRS.

            I was referring to the ability to cache in front of data source which you can do with either.

            It’s not only about caching. Load-balancing is an important technique as well, which is much easier to implement if your reads and writes are separated on the API level.

          • gregyoung

            The load balancing etc is quite useful but they can be separated as well as get/post most support this out of the box.

            The one that is really missing here is not the low end its hitting event sourcing. #3 is the naive version of getting multiple stores working. The “synchronization process” can pose many issues. Most then move towards events being pushed over say a service bus this has issues of its own. Then there is using events as storage. There are 2-3 common variations past #3.

          • http://enterprisecraftsmanship.com/ Vladimir Khorikov

            Most then move towards events being pushed over say a service bus

            This is what #3 is about actually, it’s about using pub-sub techniques for pushing changes further. I would say it’s good enough if you don’t care about the order in which the messages are received.

            But I think I should probably add #4 with event sourcing, as it unlocks 2 other benefits: the “out of box” ability to see the transaction history and message order reliability.

          • gregyoung

            There are many other issues in #3. I normally start with that in showing people event sourcing.

          • http://enterprisecraftsmanship.com/ Vladimir Khorikov

            Do you have an article or maybe a short summary with them? I could update the post with your points then.