Architecture

The idea for CQRS was born in 2010 when Greg Young (Greg Young) published an article on this topic. CQRS quickly became popular in application development, and today is one of the key approaches when working with complex systems.

CQRS (Command Query Responsibility Segregation) is an architectural pattern that proposes to separate the operations of writing and reading data in an application into two separate branches. Instead of using a single interface for both operations, CQRS suggests using different data models for commands and queries. This allows you to optimize each model for specific tasks and improve application performance.

The use of CQRS can be especially useful in systems with a large number of write operations or where distributed query processing is required. CQRS can also make it easier to maintain an application, since changes in one part of the system will not affect other parts.

While CQRS can be a complex architecture to get started, there are many benefits that can be achieved when applied correctly.

Principles of CQRS

Consider the basic principles.

1. Separation of commands and requests
One of the main principles of CQRS is the separation of commands (changing state) and requests (getting state) into different models. This allows you to use different approaches to processing each of them and improve the performance and scalability of applications.

Commands are usually processed using synchronous requests, which allows for more precise control over state changes and ensures higher data consistency. Requests, on the other hand, can be processed using asynchronous requests, which improves application performance and scalability.

Code example (code if anything in C#):

// Команда для добавления нового продукта в каталог
public class AddProductCommand
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

// Обработчик команды для добавления нового продукта в каталог
public class AddProductCommandHandler
{
    private readonly IProductRepository _productRepository;

    public AddProductCommandHandler(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public void Handle(AddProductCommand command)
    {
        var product = new Product { Name = command.Name, Price = command.Price };
        _productRepository.Add(product);
    }
}

// Запрос для получения списка всех продуктов в каталоге
public class GetAllProductsQuery
{
}

// Обработчик запроса для получения списка всех продуктов в каталоге
public class GetAllProductsQueryHandler
{
    private readonly IProductRepository _productRepository;

    public GetAllProductsQueryHandler(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public IEnumerable<Product> Handle(GetAllProductsQuery query)
    {
        return _productRepository.GetAll();
    }
}

This example shows how different models can be used to process commands and requests. Team AddProductCommand handled with a synchronous request in the handler AddProductCommandHandlerand the request GetAllProductsQuery handled with a synchronous request in the handler GetAllProductsQueryHandler.

2. Rejection of ORM
CQRS abandons ORM (Object-Relational Mapping) in favor of using aggregates (Aggregates), which represent related entities in the application and contain the logic for their state change. This improves performance and scalability, as the ORM can become a bottleneck when dealing with large amounts of data.

Aggregates contain logic for changing state, which allows for more precise control over changes and ensures higher data consistency. Each aggregate can have its own lifecycle and state.

Code example (C#):

// Агрегат для заказа
public class OrderAggregate
{
    private List<OrderLine> _orderLines = new List<OrderLine>();

    public void AddOrderLine(OrderLine orderLine)
    {
        // Добавление новой позиции в заказ
        _orderLines.Add(orderLine);
    }

    public void RemoveOrderLine(OrderLine orderLine)
    {
        // Удаление позиции из заказа
        _orderLines.Remove(orderLine);
    }

    public decimal GetTotal()
    {
        // Вычисление общей суммы заказа по всем позициям
        return _orderLines.Sum(ol => ol.Price * ol.Quantity);
    }
}

// Сущность для позиции заказа
public class OrderLine
{
    public string ProductName { get; set; }
    public decimal Price { get; set; }
    public int Quantity { get; set; }
}

Here we looked at how aggregates can be used to represent related entities in an application. Unit OrderAggregate contains a list of order items (OrderLine) and the logic of their state change. Method GetTotal uses this list to calculate the total order amount.

3. Asynchronous processing
CQRS uses asynchronous processing to improve application performance and scalability. Asynchronous processing allows you to perform multiple operations at the same time, which speeds up the processing of requests and commands.

Code example:

// Асинхронный запрос для получения списка всех продуктов в каталоге
public class GetAllProductsQueryAsync
{
}

// Асинхронный обработчик запроса для получения списка всех продуктов в каталоге
public class GetAllProductsQueryHandlerAsync
{
    private readonly IProductRepository _productRepository;

    public GetAllProductsQueryHandlerAsync(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public async Task<IEnumerable<Product>> Handle(GetAllProductsQueryAsync query)
    {
        return await _productRepository.GetAllAsync();
    }
}

Request GetAllProductsQueryAsync handled with an async Handle method that uses an async method GetAllAsync to get a list of all products in the catalog.

Components of CQRS

CQRS components can be divided into two categories: write components and read components.

Recording components include:

1 Command is an object that contains the data needed to perform a write operation on the system. Command can be sent from any part of the system, such as from a web application or from another service. In C#, the Command Object pattern is often used to create a Command:

public class CreateOrderCommand
{
    public string CustomerName { get; set; }
    public decimal TotalAmount { get; set; }
}

2. Command Handler is a component that receives a Command and performs a write operation on the system. The Command Handler receives the data from the Command and passes it to the Write Model to save the changes to the database. C# uses the Command Handler pattern to process a Command Handler:

public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand>
{
    private readonly IOrderRepository _orderRepository;

    public CreateOrderCommandHandler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public void Handle(CreateOrderCommand command)
    {
        var order = new Order
        {
            CustomerName = command.CustomerName,
            TotalAmount = command.TotalAmount
        };
        _orderRepository.Save(order);
    }
}

3. Write Model is the data model that is used for write operations in the system. The Write Model contains the data that is needed to save changes to the database. In C#, the Repository pattern is often used to create a Write Model:

public class Order
{
    public int OrderId { get; set; }
    public string CustomerName { get; set; }
    public decimal TotalAmount { get; set; }
}

4.Event

is an object that contains data about the changes that have occurred in the system after the write operation was performed. The event can be sent to other parts of the system that might be interested in these changes. In C#, the Domain Event pattern is often used to create an Event:

public class OrderCreatedEvent
{
    public int OrderId { get; set; }
    public string CustomerName { get; set; }
    public decimal TotalAmount { get; set; }
}

Reading components include:

1.Read Model is a data model that is meant to be read. The Read Model contains only the data that is needed to display to the user or to execute queries. The Read Model can be optimized for specific requests to improve system performance:

public class OrderReadModel
{
    public int OrderId { get; set; }
    public string CustomerName { get; set; }
    public decimal TotalAmount { get; set; }
}

2. Event Handler

is a component that receives an Event and updates the data in the Read Model in accordance with the changes that have occurred in the system. The Event Handler receives the data from the Event and updates the Read Model to reflect these changes. In C#, the Event Handler pattern is used to process an Event Handler.

public class OrderReadModelGenerator : IEventHandler<OrderCreatedEvent>, IEventHandler<OrderUpdatedEvent>
{
    private readonly List<OrderReadModel> _orders = new List<OrderReadModel>();

    public void Handle(OrderCreatedEvent @event)
    {
        var order = new OrderReadModel
        {
            OrderId = @event.OrderId,
            CustomerName = @event.CustomerName,
            TotalAmount = @event.TotalAmount
        };
        _orders.Add(order);
    }

    public void Handle(OrderUpdatedEvent @event)
    {
        var order = _orders.FirstOrDefault(x => x.OrderId == @event.OrderId);
        if (order != null)
        {
            order.CustomerName = @event.CustomerName;
            order.TotalAmount = @event.TotalAmount;
        }
    }

    public List<OrderReadModel> GetOrders()
    {
        return _orders;
    }
}

3. Query

is an object that contains the data required to perform a read operation on the system. The Query can be sent from any part of the system, such as from a web application or from another service. C# often uses the Query Object pattern to create a Query.

public class GetOrdersQuery
{
}

4. Query Handler

is a component that receives a Query and returns data from the Read Model in accordance with the request. The Query Handler takes the data from the Read Model and returns it as a list or other format that can be used to display the data to the user or to execute read queries. C# uses the Query Handler pattern to process a Query Handler.

public class GetOrdersQueryHandler : IQueryHandler<GetOrdersQuery, List<OrderReadModel>>
{
    private readonly OrderReadModelGenerator _readModelGenerator;

    public GetOrdersQueryHandler(OrderReadModelGenerator readModelGenerator)
    {
        _readModelGenerator = readModelGenerator;
    }

    public List<OrderReadModel> Handle(GetOrdersQuery query)
    {
        return _readModelGenerator.GetOrders();
    }
}

The components of CQRS can be implemented in different programming languages, but the basic principles remain the same.

Advantages and disadvantages

Advantages:

Improved performance: In projects with large amounts of data and high server load, CQRS allows you to separate the logic of commands and queries into separate components so that query processing is faster and more efficient.

More flexible design: CQRS provides more flexibility in application design and architecture, allowing developers to create more precise and specialized data models that better fit the requirements of a particular application.

Ease of Testing: Separating command and query logic allows for more efficient testing of each component separately, making testing easier and bug detection easier.

Increased fault tolerance: CQRS allows you to create more fault-tolerant applications by separating the read and write components, which prevents the entire system from crashing if a problem occurs with one of the components.

Flaws:

Greater Complexity: CQRS requires a more complex architecture and more skilled developers, which can make it difficult to develop and maintain applications.

Additional costs: The implementation of CQRS may require additional development and support costs, which may affect the project budget.

Not suitable for all applications: CQRS may not be suitable for all types of applications, especially those that do not have complex command and query logic or projects with small amounts of data.

Example implementation of CQRS

Let’s take as a basis a fictitious project of an online store that sells electronics.

Project description:
The project consists of several components: a web application, an application server, a database, and an analytics and monitoring system. The web application allows users to view the product catalog, place orders and pay for them. The application server is responsible for processing requests, validating data, and generating responses based on data from the database. The analytics and monitoring system tracks site visit statistics, sales numbers and other metrics.

To implement the CQRS architecture, we will divide our system into two parts: command and query.

Write Model:
The command part of the system (Write Model) is responsible for processing commands from the user, for example, creating or updating an order. Commands are processed using Command Handlers, which check the correctness of the data and store it in the Event Store.

Command Handler to create an order:

public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand>
{
    private readonly IEventStore _eventStore;

    public CreateOrderCommandHandler(IEventStore eventStore)
    {
        _eventStore = eventStore;
    }

    public async Task HandleAsync(CreateOrderCommand command)
    {
        var order = new Order(command.OrderId, command.UserId, command.Products);
        await _eventStore.SaveAsync(order);
    }
}

Read Model implementation to get a list of products:

public class ProductReadModel
{
    private readonly IDbConnection _dbConnection;

    public ProductReadModel(IDbConnection dbConnection)
    {
        _dbConnection = dbConnection;
    }

    public async Task<IEnumerable<Product>> GetProductsAsync()
    {
        var products = await _dbConnection.QueryAsync<Product>("SELECT Id, Name, Price FROM Products");
        return products;
    }
}

event store is a store of events that reflect changes in the system. Each event represents a change in the state of the system, such as creating an order or changing the status of an order. Events are stored in chronological order and can be used to restore the state of the system at any point in time.

Event Store code to save events:

public class EventStore : IEventStore
{
    private readonly IDbConnection _dbConnection;

    public EventStore(IDbConnection dbConnection)
    {
        _dbConnection = dbConnection;
    }

    public async Task SaveAsync<T>(T @event) where T : IEvent
    {
        await _dbConnection.ExecuteAsync("INSERT INTO Events (Type, Data) VALUES (@type, @data)", new { type = @event.GetType().Name, data = JsonConvert.SerializeObject(@event) });
    }

    public async Task<IEnumerable<T>> GetEventsAsync<T>(Guid aggregateId) where T : IEvent
    {
        var events = await _dbConnection.QueryAsync<Event>("SELECT Type, Data FROM Events WHERE AggregateId = @aggregateId", new { aggregateId });
        return events.Select(e => JsonConvert.DeserializeObject<T>(e.Data));
    }
}

Results:

The division into command and query parts has improved the performance and scalability of the system. The data in the Read Model is updated only when necessary, which avoids frequent queries to the database and speeds up the display of data to the user. In addition, using Event Sourcing allows you to restore the state of the system at any point in time.

Conclusion

The CQRS architecture makes the system more flexible, scalable and productive. However, its use requires additional efforts to develop and maintain the system. When used correctly, CQRS can be a powerful tool for building complex systems.

This article was prepared in anticipation of the start of the course Enterprise Architect. Learn more about the course and register for a free lesson at this link.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *