CQRS na prática: separando leitura e escrita no seu sistema

CQRS na prática: separando leitura e escrita no seu sistema

CQRS na prática: separando leitura e escrita no seu sistema

Introdução

A arquitetura de software está cada vez mais sendo questionada e reavaliada pela comunidade de desenvolvedores, especialmente em relação às maneiras como os sistemas lidam com as operações CRUD (Create, Read, Update, Delete). Diversas abordagens foram propostas ao longo dos anos, cada uma tentando resolver problemas específicos que surgem quando se trabalha com esses tipos de operações.

Uma dessas abordagens é o padrão CQRS (Command Query Responsibility Segregation), que visa separar a responsabilidade de lidar com requisições de leitura e escrita em sistemas de software. Este artigo aborda a aplicação do CQRS em prática, explorando como essa abordagem pode ajudar a melhorar a performance, escalabilidade e manutenibilidade dos sistemas.

Dentro das páginas que se seguem, você aprenderá sobre os conceitos básicos do CQRS, incluindo como separamos comandos (Command) de consultas (Query), como estruturar as responsabilidades em nossos sistemas e quais são os principais benefícios da adoção desse padrão. Além disso, exploraremos algumas implementações práticas para que você possa começar a aplicar esses conceitos nos seus próprios projetos.

O que é e por que importa

O padrão CQRS foi apresentado pela primeira vez na conferência de desenvolvimento de software LMAX Exchange em 2010, como uma abordagem para lidar com as operações CRUD de maneira eficiente e escalável. Ele se baseia no princípio de separar a responsabilidade de lidar com requisições de leitura (Query) da responsabilidade de lidar com requisições de escrita (Command).

A motivação por trás do CQRS é resolver os problemas relacionados à unificação das operações CRUD em um único conjunto de código, o que pode causar baixa performance e dificuldades de escalabilidade. Ao separar as responsabilidades, é possível otimizar cada tipo de requisição de acordo com suas necessidades específicas.

O CQRS utiliza dois principais conceitos:

  • Command: Representa uma ação ou mudança no estado do sistema, como criar, atualizar ou deletar dados. Os comandos são processados pela camada de aplicação e, em seguida, enviado para a camada de infraestrutura para serem executados.
  • Query: Representa uma solicitação de informações sobre o estado do sistema, como buscar dados ou estatísticas. As consultas são processadas pela camada de aplicação e podem ter diferentes requisitos de performance em relação aos comandos.

Ao separar os comandos das consultas, é possível otimizar a infraestrutura para cada tipo de requisição, tornando o sistema mais escalável e performático. Além disso, essa abordagem também melhora a manutenibilidade do código, pois as responsabilidades são bem definidas e isoladas.

Como funciona na prática

O CQRS é implementado por meio de várias camadas no sistema, cada uma responsável por processar e armazenar as informações de forma eficiente.

Aqui estão as etapas principais que envolvem a implementação do CQRS:

1. Requisição de Comando

  • Quando um usuário solicita uma alteração no estado do sistema (por exemplo, criar ou atualizar dados), o sistema processa essa requisição e envia um comando para a camada de infraestrutura.
  • Esses comandos são processados pela Camada de Aplicação, que é responsável por validá-los e determinar quaisquer alterações necessárias no estado do sistema.

2. Processamento do Comando

  • A Camada de Infraestrutura recebe o comando e executa as ações necessárias para implementar os comandos.
  • Para isso, ela pode usar tecnologias como bases de dados transacionais, sistemas de gerenciamento de mensagens ou até mesmo operações em nuvem, dependendo das necessidades do sistema.

3. Atualização dos Dados

  • Após processar o comando, a Camada de Infraestrutura atualiza os dados no armazenamento.
  • Isso pode incluir operações simples como atualizar registros ou complexas como executar procedimentos de banco de dados ou incluso operações em sistemas externos.

4. Requisição de Consulta

  • Quando um usuário solicita informações sobre o estado do sistema (por exemplo, buscar dados ou estatísticas), o sistema processa essa requisição e envia uma consulta para a camada de infraestrutura.
  • Para que isso seja possível, as consultas são armazenadas no Banco de Dados de Consulta, um conceito chave do CQRS.

5. Processamento da Consulta

  • A Camada de Infraestrutura processa a consulta e retorna os dados solicitados.
  • Para isso, ela pode usar tecnologias como bancos de dados de alta performance, sistemas de gerenciamento de mensagens ou até mesmo operações em nuvem.

6. Retorno da Requisição

  • O sistema envia as informações retornadas pela Camada de Infraestrutura para o usuário.
  • Isso pode incluir a exibição dos dados solicitados, atualização das telas do sistema ou até mesmo disparo de eventos.

Essas são as etapas principais que compõem a implementação do CQRS. Como podemos ver, a abordagem permite uma separação clara entre as operações de leitura e escrita, tornando o sistema mais escalável e performático.

Ao processar comandos e consultas da maneira acima, é possível otimizar cada tipo de requisição de acordo com suas necessidades específicas. Com essa abordagem, os sistemas podem lidar com as operações CRUD de forma eficiente e escalável, tornando o CQRS uma ferramenta valiosa para os desenvolvedores.

Exemplo real

Vamos exemplificar a implementação do CQRS no contexto de um sistema de gerenciamento de produtos em uma loja online. O objetivo é separar as operações de leitura e escrita para melhorar a escalabilidade e performance.

Camada de Aplicação

public class ProdutoService : IProdutoService
{
    private readonly ICommandBus _commandBus;
    private readonly IQueryBus _queryBus;

    public ProdutoService(ICommandBus commandBus, IQueryBus queryBus)
    {
        _commandBus = commandBus;
        _queryBus = queryBus;
    }

    public void AdicionarProduto(AdicionarProdutoCommand command)
    {
        // Processa a requisição de comando
        _commandBus.Dispatch(command);
    }

    public ObterProdutoQueryResult BuscarProduto(ObterProdutoQuery query)
    {
        // Processa a requisição de consulta
        return _queryBus.Query(query);
    }
}

Camada de Infraestrutura

public class CommandHandler : ICommandHandler<AdicionarProdutoCommand>
{
    private readonly IProdutoRepository _produtoRepository;

    public CommandHandler(IProdutoRepository produtoRepository)
    {
        _produtoRepository = produtoRepository;
    }

    public async Task Handle(AdicionarProdutoCommand command, CancellationToken cancellationToken)
    {
        // Processa a requisição de comando
        await _produtoRepository.AdicionarProduto(command.Nome, command.Preco);
    }
}
public class QueryHandler : IQueryHandler<ObterProdutoQuery>
{
    private readonly IProdutoRepository _produtoRepository;

    public QueryHandler(IProdutoRepository produtoRepository)
    {
        _produtoRepository = produtoRepository;
    }

    public async Task<ObterProdutoQueryResult> Handle(ObterProdutoQuery query, CancellationToken cancellationToken)
    {
        // Processa a requisição de consulta
        var produto = await _produtoRepository.BuscarProduto(query.Id);
        return new ObterProdutoQueryResult { Nome = produto.Nome, Preco = produto.Preco };
    }
}

Nesse exemplo, o serviço ProdutoService atua como ponte entre a camada de aplicação e as camadas de infraestrutura. Ele recebe comandos e consultas da camada de aplicação e os encaminha para as respectivas camadas de infraestrutura.

A camada de comando processa as requisições de comando, adicionando produtos ao sistema. Já a camada de consulta processa as requisições de consulta, retornando informações sobre produtos existentes no sistema.

Essa implementação permite uma separação clara entre operações de leitura e escrita, tornando o sistema mais escalável e performático.

Boas práticas

  • Uma ponte por vez: Evite a criação de serviços que atuam como pontes entre várias camadas, pois isso pode levar a um acúmulo de dependências e dificultar a manutenção do sistema.
  • Interface bem definida: Certifique-se de que as interfaces dos handlers de comando e consulta sejam bem definidas e claras, facilitando a compreensão da funcionalidade de cada componente.
  • Testabilidade: Priorize a testabilidade ao desenhar os handlers de comando e consulta, garantindo que possam ser facilmente testados em isolamento.

Armadilhas comuns

  • Over-engineering: Evite o excesso de complexidade ao implementar CQRS, pois isso pode levar a um acúmulo de dependências e dificultar a manutenção do sistema.
  • Desbalanceamento das camadas: Preste atenção para não desequilibrar as camadas de comando e consulta, garantindo que ambas sejam escaláveis e performáticas.

Conclusão

A implementação do CQRS na prática oferece benefícios significativos ao desenhar sistemas escaláveis e performáticos. Ao separar leitura e escrita em camadas distintas, é possível otimizar cada operação e garantir que o sistema atenda às necessidades de alto tráfego e grandes volumes de dados.

Para implementar CQRS de forma eficaz, é essencial priorizar a clareza e a testabilidade das interfaces dos handlers de comando e consulta. Além disso, é importante evitar a criação de serviços que atuam como pontes entre várias camadas e garantir o desequilíbrio das camadas de comando e consulta.

Se você está considerando implementar CQRS em seu próximo projeto, siga os passos abaixo:

  • Estude as boas práticas apresentadas nesse artigo e evite armadilhas comuns ao longo do caminho.
  • Desenvolva uma arquitetura de CQRS que se adapte às necessidades específicas do seu sistema e das características dos dados.
  • Priorize a documentação e os testes para garantir a estabilidade e a escalabilidade do sistema.

Além disso, é possível profundizar nos seguintes tópicos relacionados:

  • Implementação de uma camada de evento com base em eventos, que permite ao sistema reagir a mudanças no estado.
  • Desenvolvimento de um mecanismo de cache para otimizar as operações de leitura e melhorar o desempenho do sistema.
  • Integração do CQRS com outras arquiteturas de software, como Microservices ou Event-Driven Architecture.

Referências

  • Fowler, M. Command Query Separation. Disponível em: https://martinfowler.com/bliki/CQRS.html. Acesso: 2024.
  • Martin, K. Event Sourcing vs CQRS. Disponível em: https://www.thoughtworks.com/en/blog/event-sourcing-vs-cqrs. Acesso: 2024.
  • Evans, E. Domain-Driven Design Tackles the Unpredictable Customer. Disponível em: https://martinfowler.com/bliki/DDDD.html. Acesso: 2024.
  • CQRS - Command Query Responsibility Segregation. Disponível em: https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/dd972423(v=vs.100). Acesso: 2024.
  • Martin, K. Microservices vs Monoliths. Disponível em: https://www.thoughtworks.com/en/blog/microservices-vs-monoliths. Acesso: 2024.