CQRS: o que é e como aplicar

CQRS é um padrão arquitetural criado por Greg Young e tem como finalidade segregar os modelos de leitura e gravação dentro de uma aplicação.

Neste artigo são cobertos diversos tópicos sobre ele. Apesar da simplicidade da definição deste padrão, CQRS é uma abordagem de arquitetura que pode ser muito complexa para se implementar. Porém, pode trazer enormes vantagens à aplicação e ao negócio.

Vamos lá?

A relação com o padrão CQS

Apesar das siglas serem muito parecidas, CQS (Command Query Separation) é um padrão definido por Bertrand Meyer que determina que um objeto pode ter seus métodos divididos em leitura e gravação. 

Segundo Meyer, um método pode ser de leitura ou gravação, mas não os dois ao mesmo tempo. Segundo Greg Young, CQRS usa as mesmas definições de Command and Queries que Meyer utiliza, porém em CQRS os objetos são divididos em dois. Desta forma, considerando leitura e gravação, enquanto Meyer cita que métodos de um objeto devem ter uma única responsabilidade, Young eleva esta responsabilidade a nível de objetos.

Ambos os padrões podem trabalhar juntos em uma aplicação, por exemplo, uma camada de aplicação pode ser baseada em CQS invocando os objetos CQRS.

Aplicando CQRS

Vamos considerar o cenário hipotético de uma companhia aérea:

  • Durante os períodos promocionais, como o Dia do Cliente ou a Black Friday, há um grande aumento nas requisições de leitura, pois ambos passageiros novos e já cadastrados, acessam o site da companhia para acompanhar os valores das passagens. 
  • Quando há promoções, o time de marketing envia notificações em redes sociais para atrair novos clientes, e oferece descontos para quem fizer o cadastro em até um determinado horário. Devido a isso, o serviço passa a receber uma grande quantidade de requisições de gravação. 

Desta forma é possível identificar momentos em que a companhia precisa priorizar a leitura e momentos em que precisa priorizar a gravação. Este é um cenário típico para o uso de CQRS.

Formas de aplicar CQRS

Há algumas formas de aplicar CQRS, desde a forma mais simples, em que ocorre a persistência de dados na mesma base, até uma divisão completa dos dois modelos com bases de dados distintas. 

Separação dos objetos e mesma base de dados

Como Greg Young menciona, o CQRS usa as definições de Command e Query do CQS mas divide os objetos em dois modelos. Tal divisão dos objetos permite uma melhor organização do código e determina de forma clara quem faz a leitura e quem faz a gravação. 

Considerando que ambos os fluxos utilizam a mesma base de dados, esta forma permite a consistência imediata dos dados. Se considerar uma base de dados relacional, a tabela pode ser o repositório de gravação e as visões podem ser responsáveis por fornecer dados de leitura. A desvantagem deste modelo é que restringe a otimização dos recursos utilizados por ambos os modelos.

Separação dos objetos e bases de dados distintas

Este cenário adiciona complexidade ao desenvolvimento pois é necessário se preocupar com consistência eventual. Isso acontece porque os dados são gravados em uma base (gravação) e dependem de um mecanismo que faça a replicação para a outra base (leitura). A vantagem é que este modelo pode ser adotado quando há necessidade de alocação de recursos para leitura ou para gravação de forma separada. É muito comum as aplicações fazerem mais leitura do que gravação, logo priorizar a otimização das bases de leitura com alocação de mais recursos pode ser uma estratégia muito interessante até mesmo para uma gestão mais eficiente dos custos.

Separação a nível de serviço

Se a aplicação exigir alta disponibilidade, dividir a aplicação em serviços de leitura e gravação pode ser uma estratégia interessante para escalabilidade. Se for necessário aumentar os recursos de leitura, novas instâncias do serviço de leitura podem ser executadas para suprir a demanda. Se ambos os serviços usam bancos de dados distintos (como é o caso de microsserviços), a consistência eventual será algo a se preocupar.

Escolhendo uma forma

A escolha da arquitetura deve ser motivada pelas necessidades que o negócio exige. Considerando o cenário proposto para a companhia aérea, utilizar a segregação de objetos na mesma base de dados tem como benefício a consistência imediata dos dados, porém dificulta a otimização e impacta nos custos de operação da aplicação. Isso acontece porque se for necessário otimizar os recursos de escrita, seja pela aplicação ou pelo banco de dados, tanto leitura quanto gravação terão recursos disponibilizados e é possível que estes sejam desperdiçados por ociosidade.

Já o segundo modelo, que utiliza bases de dados distintas, tem como benefício a escalabilidade dos recursos de forma segregada. Além disso, esta segregação possibilita até mesmo escolher tecnologias distintas para atender leitura e gravação. Por exemplo, podemos usar o poder de consistência de um banco de dados relacional para gravação dos passageiros enquanto podemos usar performance dos indexadores de pesquisa para consulta. Porém, como mencionado antes, isso leva a aplicação a utilizar consistência eventual, uma vez que existe um intervalo de sincronia entre a base de gravação e leitura.

O terceiro modelo pode ser considerado uma evolução do segundo e permite otimizar não somente as bases de dados mas também os serviços envolvidos. Sendo assim, os times de desenvolvimento têm a possibilidade de escolher a linguagem que mais se adequa ao serviço. Este modelo traz consigo os mesmos desafios do modelo com bases diferentes, além do desafio de gerenciamento de ambos os serviços. Avançando um pouco no tema, talvez seja interessante usar um API Gateway para fornecer um ponto de acesso único aos dois serviços e controlar questões de observabilidade e segurança.

Para o cenário da companhia aérea, vamos seguir com o segundo modelo que permite escalar as bases de dados de forma independente, sem ter a complexidade de gerenciar múltiplos serviços.

CQRS para a companhia aérea

A aplicação de controle de passageiros pode ser definida em CQRS da seguinte forma:

Gravação

  • A Presentation Layer (camada de apresentação) é responsável por intermediar o tráfego da aplicação entre o mundo externo, e também por criar uma instância do objeto Command e enviar para o CommandHandler.
  • O CommandHandler fica responsável por tratar o comando, e executar as tarefas de negócio pertinentes. Tomando como exemplo a inserção de um novo passageiro, o CommandHandler pode converter o Command em um objeto Passenger, executar suas validações e enviar para o PassengerRepository para persistência. Após a persistência o CommandHandler gera um evento (PassengerCreatedEvent).
  • O EventHandler captura o evento e executa ações de persistência de dados na base de leitura. 

Para este cenário, quando o evento é enviado ao EventHandler, o fluxo de gravação é encerrado. Assim a gravação está isolada no modelo de leitura.

Leitura

  • Uma requisição é feita através da Presentation Layer e é enviada para a QueryHandler.
  • A QueryHandler pode validar os parâmetros de consulta e chamar uma classe Query para consultar a base de dados, converter os resultados em DTOs e retornar para a Presentation Layer.

O fluxo de leitura é simplificado uma vez que os dados devem estar prontos para consulta na base de dados, então os dados são convertidos em objetos da Presentation Layer e retornados.

Desafios do CQRS

Quando os processos de leitura e gravação tem bases dados separadas, alguns desafios comuns são:

  • Idempotência: principalmente para a base de leitura, é importante que a gravação nesta base seja feita de forma idempotente para que os dados não sejam duplicados.
  • Tolerância a falhas: uma vez que o dado foi persistido no modelo de gravação, é necessário garantir que este dado chegue as bases de leitura. Operações de Retry e Circuit Breaker podem ser uma estratégia para ajudar a manter a confiabilidade dos dados.
  • Consistência eventual: o ideal é que a sincronia entre os dados aconteça no menor tempo possível, mas isso pode ser acordado com a área de negócios.

Conclusão

Escolher CQRS como parte da arquitetura de um aplicação, significa assumir riscos e ter lidar com desafios, devido a complexidade da sua implementação. Porém, traz como vantagem a possibilidade de otimizar os modelos de forma separada, usando as melhores ferramentas para cada um dos modelos.

Bonus: Insights de Tecnologias

Considerando a arquitetura proposta para resolver o problema da companhia aérea podemos pensar em algumas tecnologias que podem fazer parte desta solução. Este tópico não é uma regra, apenas apresentando exemplos de algumas ferramentas que podem ser utilizadas.

Presentation Layer

A Presentation Layer pode ser desenvolvida usando  REST ou gRPC para gravação e leitura. Dependendo do caso, GraphQL pode ser usado para leitura dos dados

Commands, Events, Queries e seus Handlers

Esta camada pode ser feita com qualquer linguagem que faça sentido para o negócio e para o time de desenvolvimento. Com uma ressalva de que nem sempre o EventHandler precisa ser implementado. Por exemplo, se os eventos foram persistidos em um EventStore, pode ser interessante utilizar ferramentas CDC que geram eventos em filas do RabbitMQ ou tópicos do Kafka que são consumidos do lado de consulta para persistência dos dados.

Data Sources

Bases de dados relacionais (SQL Server, Postgres, Oracle), por ter um bom controle quanto as restrições de chaves e duplicidade de dados, podem ser interessantes para a gravação dos dados, mas nada impede a utilização de uma base não-relacional para a mesma finalidade. Bases não-relacionais (Mongo, Cassandra) e Indexadores (ElasticSearch, Solr) são bem interessantes para utilizar como bases de leitura.