CQRS Basics

    This article is based on material from various articles on CQRS, as well as projects where this approach has been applied.

    Management systems for enterprises, projects, employees have long been included in our lives. And users of such enterprise applications are increasingly demanding: scalability requirements, complexity of business logic are increasing, system requirements are changing rapidly, and reporting is required in real time.

    Therefore, during development, you can often observe the same problems in organizing code and architecture, as well as in complicating them. With the wrong design approach, sooner or later there may come a moment when the code becomes so complex and confusing that every change requires more time and resources.

    Typical Application Design Approach


    image

    Layered architecture is one of the most popular ways to structure your web application. In its simple variation, as in the above diagram, the application is divided into 3 parts: the data layer, the business logic layer and the user interface layer.

    In the data layer there is a certain Repository, which abstracts us from the data warehouse.
    There are objects in the business logic layer that encapsulate business rules (usually their names vary within Services / BusinessRules / Managers / Helpers). User requests pass from the UI through business rules, and then through the Repository, work with the data warehouse is performed.

    With this architecture, requests for receiving and modifying data are usually made in the same place - in the business logic layer. This is a fairly simple and familiar way of organizing code, and such a model can be suitable for most applications, if in these applications the number of users does not change significantly over time, the application does not experience heavy loads and does not require a significant expansion of functionality.

    But if the web resource becomes quite popular, the question may arise that one server is not enough for it. And then the question arises of load balancing between multiple servers. The easiest option to quickly distribute the load is to use multiple copies of the resource and database replication. And given that all the actions of such a system are not separated in any way, this creates new problems.

    Classical multi-layer architecture does not provide an easy solution to such problems. Therefore, it would be nice to use approaches in which these problems are solved from the very beginning. One such approach is CQRS.

    Command and Query Responsibility Segregation (CQRS)


    CQRS is a software design approach in which state-changing code is separated from code that simply reads that state. Such a separation can be logical and based on different levels. In addition, it can be physical and include different tiers, or levels.

    This approach is based on the principle of Command-query separation (CQS).

    The main idea of ​​CQS is that there can be two types of methods in an object:
    • Queries: Methods return the result without changing the state of the object. In other words, Query has no side effects .
    • Commands: Methods change the state of an object without returning a value.

    For an example of this separation, consider the User class with one IsEmailValid method:
    1. public class User
    2. {
    3.     public string Email { get; private set; }
    4.  
    5.     public bool IsEmailValid(string email)
    6.     {
    7.         bool isMatch = Regex.IsMatch("email pattern", email);
    8.  
    9.         if (isMatch)
    10.         {
    11.             Email = email; // Command
    12.         }
    13.  
    14.         return isMatch; // Query
    15.     }
    16. }

    In this method, we ask (do Query) whether the transmitted email is valid. If so, then we get True, otherwise False. In addition to returning the value, it is also determined here that in the case of a valid email, immediately assign its value (do Command) to the Email field.

    Despite the fact that the example is quite simple, a less trivial situation is also possible if you imagine the Query method, which, when called at several levels of nesting, changes the state of different objects. In the best case, you are lucky if you do not have to deal with similar methods and their long debugging. These side effects from calling Query are often discouraging, as it is difficult to understand how the system works.

    If we use the CQS principle and divide the methods into Command and Query, we get the following code:
    1. public class User
    2. {
    3.     public string Email { get; private set; }
    4.  
    5.     public bool IsEmailValid(string email) // Query
    6.     {
    7.         return Regex.IsMatch("email pattern", email);
    8.     }
    9.  
    10.     public void ChangeEmail(string email) // Command
    11.     {
    12.         if (IsEmailValid(email) == false)
    13.             throw new ArgumentOutOfRangeException(email);
    14.  
    15.         Email = email;
    16.     }
    17. }

    Now the user of our class will not see any state changes when calling IsEmailValid, he will only get the result - whether email is valid or not. And if you call the ChangeEmail method, the user will explicitly change the state of the object.

    Query has one feature in CQS. Since Query does not change the state of the object in any way, methods of the Query type can be well parallelized, separating the load attributable to read operations.

    If CQS operates on methods, then CQRS rises to the level of objects. The Command class is created to change the state of the system , and the Query class is used to fetch data . Thus, we get a set of objects that change the state of the system, and a set of objects that return data.

    A typical system design, where there is a UI, business logic and database:

    image

    CQRS says that you do not need to mix Command and Query objects, you need to explicitly select them. A system divided in this way will already look like this:

    image

    The separation pursued by CQRS is achieved by grouping query operations at one level, and teams at another. Each level has its own data model, its own set of services and is created using its own combination of templates and technologies. More importantly, these two levels can even be located in two different tiers and can be optimized separately without affecting each other.

    Just understanding that commands and queries are two different things has a profound effect on the software architecture. For example, it suddenly becomes easier to anticipate and encode each level of the subject area. The domain layer in the command stack only needs data, business rules and security rules to complete the tasks. On the other hand, the domain level in the query stack can be no more complicated than a direct SQL query.

    Where to start when working with CQRS?


    1. Command stack

    In CQRS, the command stack is assigned only to perform tasks that modify the state of the application. The command has the following properties:
    • Changes the state of the system;
    • Returns nothing;
    • The command context stores the data necessary for its execution.

    The declaration and use of the command can be conditionally divided into 3 parts:
    • A command class representing data;
    • Command handler class;
    • A class with a method or methods that take an input command and call exactly the handler that implements the command with this type.

    The essence of commands and queries is that they have a common feature by which they can be combined. In other words, they have a common type. In the case of commands, it will look like this:
    1. public interface ICommand
    2. {
    3. }

    The first step is to declare an interface that, as a rule, contains nothing. It will be used as a parameter, which can be obtained on the server side directly from the user interface (UI), or be formed in another way, for transmission to the command handler.

    Next, the command handler interface is declared.
    1. public interface ICommandHandler where TCommand : ICommand
    2. {
    3.     void Execute(TCommand command);
    4. }

    It contains only 1 method that accepts data with the interface type declared earlier.

    After this, it remains to determine how to centrally call the command handlers depending on the specific type of the transmitted command (ICommand). This role can be performed by a service bus or a dispatcher.
    1. public interface ICommandDispatcher
    2. {
    3.     void Execute(TCommand command) where TCommand : ICommand;
    4. }

    Depending on the needs, it can have either one or more methods. In simple cases, one method may be sufficient, the task of which is to determine by the type of the passed parameter which implementation of the command handler to call. Thus, the user does not have to do this manually.

    Command example . Suppose there is an online store, for it you need to create a team that will create the goods in the warehouse. First, create a class where in its name we indicate what action this command performs.
    1. public class CreateInventoryItem : ICommand
    2. {
    3.     public Guid InventoryItemid { get; }
    4.     public string Name { get; }
    5.  
    6.     public CreateInventoryItem(Guid inventoryItemld, string name)
    7.     {
    8.         InventoryItemId = inventoryItemId;
    9.         Name = name;
    10.     }
    11. }

    All classes that implement ICommand contain data - properties and a constructor with setting their values ​​during initialization - and nothing more.

    The implementation of the handler, that is, directly to the command itself, boils down to fairly simple actions: a class is created that implements the ICommandHandler interface. The type argument indicates the command declared earlier.
    1. public class InventoryCommandHandler : ICommandHandler
    2. {
    3.     private readonly IRepository _repository;
    4.  
    5.     public InventoryCommandHandlers(IRepository repository)
    6.     {
    7.         _repository = repository;
    8.     }
    9.  
    10.     public void Execute(CreateInventoryItem message)
    11.     {
    12.         var item = new InventoryItem(message.InventoryItemld, message.Name);
    13.         _repository.Save(item);
    14.     }
    15.  
    16.     // ...
    17. }

    Thus, we implement a method that takes this command as an input and indicate what actions we want to perform based on the transmitted data. Command handlers can be combined logically and implement several ICommandHandler interfaces with different types of commands in one such class. This will result in overloading the methods, and when you call the Execute method, the appropriate command type will be selected.

    Now, to call the appropriate command handler, you need to create a class that implements the ICommandDispatcher interface. Unlike the last two, this class is created once and may have different implementations depending on the strategy of registering and invoking command handlers.
    1. public class CommandDispatcher : ICommandDispatcher
    2. {
    3.     private readonly IDependencyResolver _resolver;
    4.  
    5.     public CommandDispatcher(IDependencyResolver resolver)
    6.     {
    7.         _resolver = resolver;
    8.     }
    9.  
    10.     public void Execute(TCommand command) where TCommand : ICommand
    11.     {
    12.         if (command == null) throw new ArgumentNullException("command");
    13.  
    14.         var handler = _resolver.Resolve>();
    15.  
    16.         if (handler == null) throw new CommandHandlerNotFoundException(typeof(TCommand));
    17.  
    18.         handler.Execute(command);
    19.     }
    20. }

    One way to invoke the desired command handler is to use a DI container in which all handler implementations are registered. Depending on the transmitted command, an instance will be created that processes this type of command. Then the dispatcher simply calls its Execute method.

    2. Query

    stack The query stack deals only with data extraction. Requests use the data model that best matches the data used at the presentation level. You hardly need any business rules — they usually apply to teams that change state.

    The request has the following properties:
    • Does not change the state of the system;
    • The request context stores the data necessary for its execution (paging, filters, etc.);
    • Returns the result.

    The announcement and use of requests can also be conditionally divided into 3 parts:
    • The request class, which is data with the type of the returned result;
    • Request Handler Class
    • A class with a method or methods that accept a request for input and call exactly the handler that implements a request with this type.

    As with commands, similar interfaces are declared for queries. The only difference is that they indicate what should be returned.
    1. public interface IQuery
    2. {
    3. }

    Here, the type of returned data is indicated as a type argument. It can be an arbitrary type, for example, string or int [].

    Then a request handler is declared, where the type of the return value is also indicated.
    1. public interface IQueryHandler where TQuery : IQuery
    2. {
    3.     TResult Execute(TQuery query);
    4. }

    By analogy with commands, a dispatcher is declared to call request handlers.
    1. public interface IQueryDispatcher
    2. {
    3.     TResult Execute(TQuery query) where TQuery : IQuery;
    4. }

    Request example. Suppose you want to create a query that returns users by search criteria. Here, using the meaningful class name, we indicate what kind of request will be made.
    1. public class FindUsersBySearchTextQuery : IQuery
    2. {
    3.     public string SearchText { get; }
    4.     public bool InactiveUsers { get; }
    5.  
    6.     public FindUsersBySearchTextQuery(string searchText, bool inactiveUsers)
    7.     {
    8.         SearchText = searchText;
    9.         InactiveUsers = inactiveUsers;
    10.     }
    11. }

    Next, we create a handler that implements IQueryHandler with arguments of the type of the query and the type of its return value.
    1. public class UserQueryHandler : IQueryHandler
    2. {
    3.     private readonly IRepository _repository;
    4.  
    5.     public UserQueryHandler(IRepository repository)
    6.     {
    7.         _repository = repository;
    8.     }
    9.  
    10.     public User[] Execute(FindUsersBySearchTextQuery query)
    11.     {
    12.         var users = _repository.GetAll();
    13.         return users.Where(user => user.Name.Contains(query.SearchText)).ToArray();
    14.     }
    15. }

    Then it remains to create a class to call request handlers.
    1. public class QueryDispatcher : IQueryDispatcher
    2. {
    3.     private readonly IDependencyResolver _resolver;
    4.  
    5.     public QueryDispatcher(IDependencyResolver resolver)
    6.     {
    7.         _resolver = resolver;
    8.     }
    9.  
    10.     public TResult Execute(TQuery query) where TQuery : IQuery
    11.     {
    12.         if (query == null) throw new ArgumentNullException("query");
    13.  
    14.         var handler = _resolver.Resolve>();
    15.  
    16.         if (handler == null) throw new QueryHandlerNotFoundException(typeof(TQuery));
    17.  
    18.         return handler.Execute(query);
    19.     }
    20. }

    Call commands and queries


    In order to be able to call commands and queries, it is enough to use the appropriate dispatchers and transfer a specific object with the necessary data. For example, it looks like this:
    1. public class UserController : Controller
    2. {
    3.     private IQueryDispatcher _queryDispatcher;
    4.  
    5.     public UserController(IQueryDispatcher queryDispatcher)
    6.     {
    7.        _queryDispatcher = queryDispatcher;
    8.     }
    9.  
    10.     public ActionResult SearchUsers(string searchString)
    11.     {
    12.         var query = new FindUsersBySearchTextQuery(searchString);
    13.  
    14.         User[] users =_queryDispatcher.Execute(query);
    15.  
    16.         return View(users);
    17.     }
    18. }

    Having a controller for processing user requests, it is enough to transfer the object of the desired dispatcher as a dependency, then form the request or command object and pass it to the dispute method Execute.

    So we get rid of the need to constantly increase dependencies with an increase in the number of system functions and reduce the number of potential errors.

    Register handlers

    Register handlers can be a variety of ways. Using the DI container, you can register individually or automatically scan the assembly with the desired types. The second option may look like this:
    using SimpleInjector;

    var container = new Container();

    container.Register(typeof(ICommandHandler<>), AppDomain.CurrentDomain.GetAssemblies());

    container.Register(typeof(IQueryHandler<,>), AppDomain.CurrentDomain.GetAssemblies());

    This uses the SimpleInjector container . When registering handlers with the Register method, the first argument indicates the type of interfaces for the command and request handlers, and the second - the assembly, in which the classes that implement these interfaces are searched. Thus, it is not necessary to specify specific handlers, but only a common interface, which is extremely convenient.

    What if, when invoking Command / Query, I need to check permissions, write information to the log, and the like?
    Their centralized call allows you to add actions before or after execution of the handler without changing any of them. It is enough to make changes to the dispatcher itself, or create a decorator that replaces the original implementation through a DI container (in the SimpleInjector documentation, examples of suchdecorators ).

    image

    Advantages of CQRS
    • Fewer dependencies in each class;
    • Respect for sole responsibility (SRP);
    • Suitable almost everywhere;
    • Easier to replace and test;
    • Functionality expands more easily.

    CQRS Limitations
    • When using CQRS, many small classes appear;
    • When using a simple CQRS implementation, it can be difficult to use a group of commands in a single transaction;
    • If common logic appears in Command and Query, you need to use inheritance or composition. This complicates the design of the system, but is not an obstacle for experienced developers;
    • It’s hard to stick to CQS and CQRS entirely. The simplest example is a method of fetching data from a stack. Data sampling is Query, but we need to change the state and make the stack size -1. In practice, you will seek a balance between strict adherence to principles and production necessity;
    • Puts bad on CRUD applications.

    Where not suitable
    • In small applications / systems;
    • In predominantly CRUD applications.

    Conclusion


    For applications to be truly effective, they must adapt to the requirements of the business. CQRS-based architecture greatly simplifies the expansion and modification of business workflows and supports new scenarios. You can manage extensions in complete isolation. To do this, just add a new handler, register and tell him how to process messages of only the required type. A new component will be automatically called only when a message appears and will work side by side with the rest of the system. Easy, simple and effective.

    CQRS allows you to optimize the pipelines of commands and queries in any way. At the same time, optimization of one conveyor will not disrupt the operation of another. In the most basic form of CQRS, one common database is used and different modules are called for read and write operations from the application layer.

    Sources
    Alexander Byndu's blog - CQRS in practice
    At the forefront - CQRS for a regular application
    How we tried DDD, CQRS and Event Sourcing and what conclusions were drawn
    CQRS Documents by Greg Young
    Simple CQRS example
    DDDD, CQRS and Other Enterprise Development Buzz-words

    Also popular now: