Introdução ao Padrão CQRS
O padrão Command Query Responsibility Segregation (CQRS) é uma abordagem arquitetural que separa as operações de leitura e escrita em uma aplicação. Essa separação permite otimizar cada operação de forma independente, levando a uma melhor escalabilidade e desempenho. Em vez de ter um modelo de dados unificado, o CQRS propõe que as operações de escrita (comandos) e leitura (consultas) sejam tratadas de maneiras distintas (RICHTER, 2024).
O CQRS é especialmente útil em aplicações complexas e de grande escala, onde a lógica de negócios pode ser intensiva e as operações de leitura podem ser otimizadas de forma diferente das operações de gravação (RIBEIRO, 2024). Neste artigo, vamos explorar como implementar CQRS em APIs Web utilizando .NET Core, abordando conceitos, componentes, e exemplos práticos para facilitar a compreensão da implementação.
Origem do CQRS
O CQRS é um padrão que surgiu como uma extensão do padrão de arquitetura de microserviços, visando resolver alguns dos desafios que surgem quando se tenta escalar aplicações que realizam tanto operações de leitura quanto de escrita (AL FANSHA et al., 2021). A ideia central do CQRS é dividir as responsabilidades de leitura e escrita, permitindo que cada uma delas seja otimizada de maneira independente (RIBEIRO, 2024).
Essa abordagem promove uma melhor organização do código, uma vez que as partes do sistema que lidam com a leitura de dados não precisam se preocupar com a lógica de escrita, e vice-versa. Além disso, o CQRS permite que diferentes tecnologias sejam utilizadas para leitura e escrita, aproveitando as melhores práticas e ferramentas disponíveis para cada operação.
O CQRS é particularmente vantajoso em cenários onde a aplicação precisa lidar com altos volumes de dados e um grande número de usuários simultâneos (CHERIF et al., 2024). A separação de comandos e consultas não apenas melhora a performance, mas também facilita a manutenção e a evolução do sistema ao longo do tempo.
Componentes Básicos do CQRS
Em uma arquitetura CQRS, geralmente lidamos com três componentes principais: comandos, consultas e eventos. Vamos detalhar cada um deles:
- Comandos: São operações que alteram o estado da aplicação. Cada comando deve ser tratado de forma assíncrona para garantir que a aplicação possa continuar a responder a outras requisições enquanto aguarda a conclusão da operação.
- Consultas: São solicitações que recuperam dados do sistema sem alterar seu estado. As consultas podem ser otimizadas para fornecer resultados rápidos e eficientes.
- Eventos: Representam mudanças que ocorreram no sistema e podem ser utilizados para notificar outros componentes ou sistemas. Eventos são fundamentais para implementar a comunicação entre diferentes partes do sistema e podem facilitar a integração com outros serviços externos.
Estrutura de um Projeto .NET Core com CQRS
Para ilustrar a atuação do padrão CQRS, vamos considerar um projeto básico em .NET Core. Utilizaremos o ASP.NET Core Web API. O design de implementação ficará dessa forma:
+-----------------------------------------------------+
| |
| MyCQRSApp |
| |
+------+---------------------------------------+------+
| |
| |
+------v------+ +------v------+
| Command | | Get |
| Actions | | Actions |
+------+------+ +------+------+
| |
| |
+------v------+ +-------------+ +------v------+
| Command | | [Producer] | | Query |
| Bus +-----> Async | | (IQueryable)|
| | | Messages | | |
+-------------+ +-------------+ +------+------+
| |
| |
+------v------+ +----------v------+
| Write DB | | Read DB |
| (Event | | (Projection/ |
| Sourcing) | | Read Database) |
+-------------+ +-----------------+
Abaixo a estrutura do projeto:
MyCQRSApp
¦
+-- Application
¦ +-- Commands
¦ ¦ +-- CreateProductCommand.cs
¦ ¦ +-- CommandHandlers
¦ ¦ +-- CreateProductCommandHandler.cs
¦ +-- Queries
¦ ¦ +-- GetProductQuery.cs
¦ ¦ +-- QueryHandlers
¦ ¦ +-- GetProductQueryHandler.cs
¦ +-- Events
¦ +-- ProductCreatedEvent.cs
¦
+-- Infrastructure
¦ +-- Bus
¦ ¦ +-- CommandBus.cs
¦ ¦ +-- EventBus.cs
¦ +-- Persistence
¦ ¦ +-- ProductService.cs
¦ ¦ +-- ProductRepository.cs
¦ +-- Messaging
¦ +-- IEventBus.cs
¦ +-- ICommandBus.cs
¦ +-- InMemoryEventBus.cs
¦
+-- API
¦ +-- Controllers
¦ +-- ProductsController.cs
¦
+-- Domain
+-- Models
+-- Product.cs
Implementando Comandos
O padrão CQRS (Command Query Responsibility Segregation) sugere separar as operações de leitura (queries) das operações de escrita (commands). Vamos começar implementando os comandos, que representam ações ou mudanças de estado que queremos realizar em nosso sistema. Cada comando é uma solicitação para executar uma ação, como criar ou atualizar um recurso. No exemplo a seguir, criaremos um comando para adicionar um produto:
public class CreateProductCommand
{
public string Name { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
O comando CreateProductCommand
contém as propriedades necessárias para criar um produto, como nome, preço e quantidade. Agora, precisamos de um manipulador de comando, que será o responsável por processar esse comando e executar a lógica de negócios associada.
O manipulador de comando será responsável por instanciar o produto, adicionar o mesmo ao banco de dados e, em seguida, disparar um evento para notificar que o produto foi criado. Veja como isso funciona:
public class CreateProductCommandHandler : ICommandHandler
{
private readonly ProductService _productService;
private readonly IEventBus _eventBus; // Event bus para publicar eventos
public CreateProductCommandHandler(ProductService productService, IEventBus eventBus)
{
_productService = productService;
_eventBus = eventBus;
}
public async Task Handle(CreateProductCommand command)
{
var product = new Product
{
Name = command.Name,
Price = command.Price,
Quantity = command.Quantity
};
await _productService.AddProductAsync(product);
// Publica o evento após a criação do produto
var productCreatedEvent = new ProductCreatedEvent(product.Id, product.Name);
await _eventBus.PublishAsync(productCreatedEvent);
}
}
O CreateProductCommandHandler
recebe o comando, cria um novo objeto Product
e o adiciona ao banco de dados utilizando o serviço ProductService
. Após a criação, um evento chamado ProductCreatedEvent
é disparado, notificando outras partes do sistema que o produto foi criado.
O evento ProductCreatedEvent
carrega as informações necessárias para notificar o sistema de que um produto foi criado com sucesso. Esse evento pode ser assinado por outros componentes do sistema para executar ações subsequentes, como atualizar caches, enviar notificações ou registrar logs.
Implementando Consultas
O próximo passo está em implementar as consultas. Ao contrário dos comandos, que alteram o estado do sistema, as consultas são responsáveis por recuperar dados. Vamos criar um exemplo de consulta para obter as informações de um produto específico a partir de seu ID.
public class GetProductQuery
{
public int ProductId { get; set; }
public GetProductQuery(int productId)
{
ProductId = productId;
}
}
O comando GetProductQuery
contém o ID do produto que estamos buscando. Agora, precisamos de um manipulador de consulta, que será o responsável por buscar as informações do produto.
public class GetProductQueryHandler
{
private readonly ProductService _productService;
public GetProductQueryHandler(ProductService productService)
{
_productService = productService;
}
public async Task Handle(GetProductQuery query)
{
return await _productService.GetProductByIdAsync(query.ProductId);
}
}
O GetProductQueryHandler
recebe o comando de consulta e usa o serviço ProductService
para recuperar o produto correspondente ao ID fornecido. Essa consulta pode ser otimizada com técnicas como cache, dependendo das necessidades do sistema.
Controladores de API
Com os comandos e consultas implementados, o próximo passo está em criar os controladores de API para expor essas operações via HTTP. Abaixo, um controlador para os produtos, com endpoints para criar e obter produtos.
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly ICommandBus _commandBus; // Barramento de comandos
private readonly IQueryHandler _getProductHandler;
public ProductsController(ICommandBus commandBus, IQueryHandler getProductHandler)
{
_commandBus = commandBus;
_getProductHandler = getProductHandler;
}
[HttpPost]
public async Task CreateProduct([FromBody] CreateProductCommand command)
{
// Envia o comando para o barramento de comandos
await _commandBus.SendAsync(command);
return Ok();
}
[HttpGet("{id}")]
public async Task GetProduct(int id)
{
var query = new GetProductQuery(id);
var product = await _getProductHandler.Handle(query);
return Ok(product);
}
}
O controlador ProductsController
expõe dois endpoints: um para criar produtos com o método POST
e outro para consultar produtos com o método GET
. O controlador interage com os barramentos de comandos e consultas para delegar as operações adequadas.
Gerenciamento de Eventos
O CQRS também é frequentemente usado em conjunto com eventos, onde a execução de um comando dispara um evento correspondente. Esses eventos podem ser usados para notificar outras partes do sistema sobre alterações de estado. No exemplo abaixo, vamos criar um evento ProductCreatedEvent
, que será disparado após a criação de um produto.
public class ProductCreatedEvent
{
public int ProductId { get; private set; }
public string Name { get; private set; }
public ProductCreatedEvent(int productId, string name)
{
ProductId = productId;
Name = name;
}
}
Este evento contém informações sobre o produto criado, como seu ID e nome. Após a criação do produto, o evento será publicado para que outros componentes do sistema possam reagir a essa mudança de estado, como atualizar caches, enviar notificações ou realizar outras ações de integração.
A publicação e assinatura de eventos podem ser gerenciadas utilizando bibliotecas como MediatR, RabbitMQ, Azure Service Bus ou outras soluções baseadas em mensageria. O uso de eventos melhora a escalabilidade do sistema, permitindo que ele reaja de forma mais dinâmica e flexível a mudanças no estado do sistema.
Desafios e Considerações ao Implementar CQRS
Apesar das vantagens do CQRS, é importante estar ciente de alguns desafios que podem surgir ao implementá-lo. A complexidade adicional que o CQRS traz pode ser um obstáculo, especialmente em projetos menores ou em equipes com menos experiência na abordagem. A gestão de estados e a sincronia entre os componentes de leitura e escrita podem se tornar complicadas, exigindo um bom planejamento e uma arquitetura bem definida.
É essencial ter uma comunicação clara entre os membros da equipe para garantir que todos entendam as responsabilidades e como cada componente interage com os outros. Além disso, é recomendável ter um bom monitoramento e logging em produção para detectar problemas rapidamente.
Conclusão
O padrão CQRS oferece uma maneira poderosa de estruturar aplicações complexas, separando as preocupações de leitura e escrita. Ao implementar CQRS em uma aplicação .NET Core, você pode criar APIs mais escaláveis, flexíveis e de alto desempenho. Embora a implementação do CQRS possa adicionar complexidade, os benefícios em aplicações de grande escala frequentemente superam esses desafios.
No final, a adoção do CQRS deve ser uma decisão informada, baseada nas necessidades específicas do seu projeto. Com a prática e a experiência adequadas, você pode aproveitar ao máximo esse padrão e construir aplicações que atendam às crescentes demandas do mercado atual.
Referências
- RICHTER, Jens. Performance Impact of the Command Query Responsibility Segregation (CQRS) Pattern in C# Web APIs. 2024. Tese de Doutorado. Universitäts-und Landesbibliothek Sachsen-Anhalt.
- AL FANSHA, Difa; SETYAWAN, Muhammad Yusril Helmi; FAUZAN, Mohamad Nurkamal. Load Test pada Microservice yang menerapkan CQRS dan Event Sourcing. Jurnal Buana Informatika, v. 12, n. 2, p. 126-134, 2021.
- CHERIF, Ayman NAIT et al. CQRS and Blockchain with Zero-Knowledge Proofs for Secure Multi-Agent Decision-Making. International Journal of Advanced Computer Science & Applications, v. 15, n. 11, 2024.
- RIBEIRO, Eduardo Renani. Alternativas para consistência eventual em um sistema no padrão CQRS. 2024.