CQRS (Command Query Responsibility Segregation) em uma Arquitetura de Microsserviços

Marcelo M. Gonçalves
14 min readFeb 18, 2020

--

Aplicar estilos e padrões arquiteturais adequados em nossos componentes de software demonstram maturidade em sua base de construção. A decisão correta demanda esforço de maneira a prosperar, e sem dúvida, a escolha certa torna-se um grande ponto de partida para o sucesso de nossa aplicação, naturalmente facilitando adoção posterior de outros padrões/conceitos que estejam preparados para enfrentarmos as dificuldades relacionadas às aplicações distribuídas.

Independente do padrão de implementação adotado em nossas API’s/componentes, seja RESTful/CRUD/EDA (event-driven architecture), naturezas de comunicação síncronas/assíncronas, quando utilizada a Arquitetura de Microsserviços o padrão CQRS poderá agregar benefícios como veremos a seguir. Além disso, aplicado juntamente a outros conceitos complementares (Event Sourcing, DDD) atribuem a nossas aplicações um diferencial competitivo quando abraçamos os principais desafios pertencentes aos sistemas distribuídos.

CQRS (command query responsibility segregation) trata-se de um padrão arquitetural escalável, propondo a separação das responsabilidades em canais de comunicação distintos, descritos como modelo de Escrita (command) e leitura (query). Em sua essência, o CQRS visa segregar as atividades exercidas pelos componentes do software, entre Comandos e Consultas.

Desta forma, tendo modelos separados conceitualmente e algumas vezes, fisicamente (envolvendo databases) torna-se possível adquirir diversas vantagens em relação a aplicações CRUD (escrita, leitura, edição e exclusão) tradicionais como veremos mais adiante. De forma que, ao adotarmos CQRS e seus conceitos de arquitetura, comandos ficam responsáveis por alterar o estado da aplicação, podendo gerar eventos refletindo determinadas mudanças, ao mesmo tempo em que Consultas (queries) podem ser usadas para resgatar o estado atual da aplicação sem alterá-lo.

CQRS como Padrão Arquitetural

CQRS não trata-se de um um estilo de arquitetura, e sim de um padrão arquitetural. Facilitando aplicarmos seus conceitos somente em determinadas partes da aplicação. CQRS encontra-se relacionado ao padrão CQS (separação de comando-consulta) onde foi originalmente descrito por Bertrand Meyer em seu livro Object Oriented Software Construction (construção de software orientada a objeto).

CQRS + Event Sourcing

CQRS (command query responsibility segregation) foi primeiramente definido por Greg Young e promovido largamente por Udi Dahan (referência SOA — service-oriented architecture). Separando em modelos distintos, podendo residir inclusive dentro do mesmo micro serviço, ao mesmo tempo em que poderiam residir em processos diferentes, podendo escalar separadamente sem impactar outros componentes. Dependendo do caso, podemos ter banco de dados desnormalizados no modelo de leitura, facilitando projeções e materialização de views na exibição dos dados.

Como qualquer design pattern de arquitetura, CQRS pode encaixar-se bem em alguns casos, mas não em outros. Desta forma, precisamos avaliar cada aplicação separadamente, conforme necessidade, sendo as decisões arquiteturais particulares para cada software.

CQRS poderia ser utilizado em porções específicas do sistema, encaixando-se em alguns bounded contexts (contextos delimitados) enquanto em outros não, quando praticado pelo time de desenvolvimento o conjunto de técnicas relacionadas ao DDD (domain-driven design).

No DDD, bounded contexts distintos possuem particularidades específicas e níveis de responsabilidades diferentes, assim, determinados bounded contexts podem aplicar CQRS enquanto outros não. CQRS é indicado para domínios complexos, podendo beneficiar-se do conjunto de padrões oferecidos pelo domain-driven design e event-sourcing, tornando a opção por estruturar sua aplicação nos moldes CQRS vantajosos. Porém, existem casos onde adotar CQRS pode adicionar riscos de complexidade na implementação e manutenção da aplicação.

CQRS vs CRUD

Abordagens tradicionais apresentam efeito negativo no desempenho da aplicação devido a carga de armazenamento de dados. Apresentam problemas na camada de acesso aos dados devido a complexidade de consultas concorrerem com validação e aplicação de regras de negócio. Modelos tradicionais realizam atividades em excesso e com sobreposição de responsabilidades.

Gerenciamento de permissões de acesso nas entidades pode ser complexo pois cada entidade estará sujeita a operações de leitura e gravação podendo expor dados em um contexto incorreto. Workload (carga de trabalho) envolvendo escrita e leitura de dados nos modelos são assimétricas, contendo requisitos de desempenho diferentes, sendo os repositórios de Leitura apresentando cargas maiores do que os repositórios de escrita. Contenção de dados podem ocorrer em operações paralelas no mesmo conjunto de dados em aplicações CRUD tradicionais, além de Incompatibilidades entre as representações dos modelos de leitura e gravação de dados, resolvidas adotando modelos separados com CQRS.

Aplicar CQRS em domínios simples, acrescenta complexidade aumentando os riscos do projeto. Sendo mais indicado para domínios complexos, ou mesmo podendo tornar-se complexos a curto prazo.

Muitas das implementações envolvendo CQRS são realizadas de maneira precoce com relação a compreensão do domínio de negócio, posteriormente sofrendo alterações acompanhado de uma visão mais madura sobre o domínio de dados. Neste caso apresentando alterações em formatos estruturais para nossa aplicação. Quando aplicado CQRS com event-sourcing, acabaremos com um tipo de aplicação arquiteturalmente complexa, onde quanto menos mudanças tivermos mais saudável para a aplicação. Por este motivo, precisamos dedicar esforços para conhecer o domínio de negócio a fundo, analisando seus conceitos e envolvendo-nos desde o início na construção da linguagem ubíqua envolvida no processo.

CQRS não deveria ser aplicado somente em casos onde simples operações de CRUD, respondendo a uma interface de usuário, atenderia o requisito sem a existência de diversas regras de negócio. Ao propor que separamos a aplicação em diferentes modelos, atualização e exibição, CQRS apresenta um mecanismo de funcionamento oposto ao modelo CRUD, onde um único modelo é utilizado para efetuar todas as operações.

Quando usado CRUD, existem tentativas de manter o modelo implementado o mais fiel possível ao armazenamento dos dados, contemplando o formato de tabelas bem como suas modelagens de mapeamentos objeto relacional (ORM). Devendo as consultas no modelo de leitura serem realizadas de forma síncrona, e as atualizações no modelo de escrita sendo realizadas de forma assíncrona. No modelo de escrita, os agregados podem inicialmente possuir um estado mínimo, o suficiente para comportar nossas regras de negócio.

Em aplicações CRUD, operações de escrita e leitura de dados são efetuadas pelo mesmo canal, podendo gerar problemas de contenção de dados, deadlocks, quedas de performance, problemas de concorrência, isolamento transacional, gerando gargalos enormes.

Historicamente, existiram diversos esforços para contorná-los em aplicações monolíticas em servidores de aplicação JEE (Java Enterprise Edition) como WebLogic Server. Em aplicações CRUD tradicionais, o banco de dados pode tornar-se um gargalo, pois tanto a escrita quanto a leitura, dependem do mesmo modelo e estrutura de tabelas as quais os joins serão executados. A tendência em aplicações CRUD é aproximar o persistent-store o máximo possível do domain-model. Ao aplicar CQRS, a tendência é distanciar-se de modelos CRUD tradicionais e consequentemente dos modelos objeto relacionais bem como seus relacionamentos ER (entidade relacionamento), gerando desacoplamento.

Na medida em que nossas necessidades tornam-se mais avançadas, nos afastamos naturalmente do modelo CRUD. A tendência seria aproximarmos de formatos onde poderemos possuir diversos tipos de apresentações representando as informações, além de separar validação de regras de negócio, e complexidades do domínio de modelos mais simples de leitura e projeção de dados.

CQRS quebra o conceito CRUD monolítico, definido em N camadas, onde todo o processo de escrita e leitura passam pelas mesmas camadas e concorrem entre processamento de regras de negócio, validação da entrada de dados, e recuperação das informações para exibição. CQRS pode ser aplicado em cenários onde necessitamos de flexibilização nos níveis de granularidade aplicada aos Comandos disparados contra domínio de dados, minimizando conflitos no processo de atualização com possibilidade de mesclagem nos comandos.

Vantagens do CQRS

Alguns dos benefícios ao aplicar CQRS incluem: dimensionamento independente entre as cargas de trabalho como leitura e escrita, otimização do esquema de dados entre modelos de leitura e escrita, cada qual aplicando o modelo mais consistente para seus tipos de utilização. Quando garantimos que apenas um modelo de dados realize gravações, garantimos maior segurança na alteração dos dados em pontos centralizados. Podemos isolar lógicas de negócio no modelo de escrita tornando o modelo de leitura relativamente simples.

O modelo de escrita (command) no CQRS pode ser combinado com padrões presentes no DDD (domain-driven design), como aggregate, bounded context, ubiquitous language, context map. Desta forma, atingindo níveis elevados de maturidade arquitetural e promovendo vantagens que determinados padrões, ao complementarem-se, beneficiam nossas aplicações.

Ao adotarmos CQRS, o modelo de escrita deve absorver a complexidade das regras de negócio, validação nos dados, podendo tratar conjuntos de dados associados a uma unidade atômica de atualização de dados. Aggregate pattern, quando aplicado DDD, garante que determinados objetos estejam em um estado consistente dentro de sua transação.

O modelo de leitura não possui lógica de negócio, devendo apenas retornar um DTO (data transfer object) para o mecanismo de exibição, e eventualmente consistente com o modelo de Escrita. Além disso, podemos atuar em tunning visando melhorar o desempenho em blocos separados, aplicando correções e adaptações isoladamente, podendo escalar horizontalmente o modelo de leitura por exemplo sem impactar o modelo de escrita. Vantagens como segregação de equipes, podem ser adquiridas, enquanto devs concentram-se em implementar regras de negócio complexas no modelo de escrita, outras equipes podem concentrar-se no modelo de Leitura relacionado a interfaces de usuário (UI).

Desafios do CQRS

Ao mesmo tempo em que benefícios são observados, alguns problemas podem ser enfrentados ao aplicarmos CQRS na prática: Uma aplicação CQRS pode apresentar design complexo, mesmo que basicamente aparente ser algo simples. A adoção do padrão arquitetural CQRS, na maior parte das vezes, funciona com base em eventos no formato de mensagens.

Na prática, falhas envolvendo esta natureza de comunicação devem ser tratadas demandando bastante atenção, além do tratamento na possível duplicação de mensagens, devendo garantir a idempotência. Lembrando que a consistência eventual deve ser garantida, quando temos modelos distintos, tanto com casos de falha quanto em casos de operações de atualização com sucesso entre bases de dados envolvidas, pois o modelo de Leitura não pode permanecer desatualizado. As abordagens envolvendo a sincronização do modelo de Leitura podem ser aplicadas, sendo a consistência eventual de natureza assíncrona a mais utilizada quando nos referimos a arquitetura de microsserviços.

CQRS ajuda a tornar o domínio mais expressivo reforçando o uso da linguagem ubíqua no modelo de Escrita, reforçando a clareza das intenções no domínio. Implementações de CQRS exigem infraestrutura de command-bus e event bus para comportar a troca de eventos entre os modelos. Possuir modelos de atualização e consulta separados fisicamente simplifica o design de implementação, aumentando o isolamento, porém devem ser mantidos em sincronia quando usados banco de dados separados.

Como boa prática, o modelo de escrita poderia publicar um evento de atualização no modelo de leitura (consulta), na mesma transação de origem, sempre que houverem atualizações. Para domínios muito complexos, segregar a escrita e a leitura de dados quebra a complexidade do modelo conceitual de domínio. Ao mesmo tempo em que modelos diferentes para atualizar e ler informações em interfaces separadas maximizam a escalabilidade e o desempenho. Neste caso, a separação representa objetos distintos em processos e componentes diferentes, pois a separação possibilita a evolução da aplicação com flexibilidade.

Quando falamos sobre CQRS precisamos mencionar as duas grandes forças que o sustentam: colaboração entre atores e dados obsoletos. Colaboração, referindo-se às circunstâncias sobre as quais diversos atores atualizarão o mesmo modelo de dados, independente se a intenção dos componentes envolvidos seja colaborar.

No mesmo contexto, dados obsoletos referem-se aos dados exibidos aos usuários ao mesmo tempo em que podem estar sendo atualizados por outro, tornando-o desatualizado. Aplicações tradicionais N camadas não lidam explicitamente com estes conceitos, pois implementam tudo no mesmo canal, seja de banco de dados ou a nível de aplicação utilizando CRUD.

CQRS parte do princípio que qualquer aplicação que faz uso de cache, por motivos de performance, estaria fornecendo dados obsoletos aos seus usuários. Desta forma, não impactando a integridade dos dados transferidos para o modelo de leitura por conta de informações desatualizadas ao adotarmos consistência eventual para sincronização de dados.

CQRS com ES (Event Sourcing)

CQRS frequentemente é utilizado em conjunto com o design pattern de persistência event-sourcing (ES). Neste caso, os eventos são armazenados no event-store de forma sequencial e imutável, tornando-se uma fonte única e original sobre a verdade da informação na linha do tempo.

O modelo de leitura em sistemas baseados em CQRS serve de base para materializações de views e projeções de dados, adaptadas aos requisitos de exibição das interfaces. Streams de eventos como repositório de gravação facilita a geração, de maneira assíncrona, de exibições materializadas visando atender o repositório de leitura de dados.

Como o repositório oficial de informações reside no modelo de gravação, pode-se recriar materializações e projeções a qualquer momento, inclusive olhada para dados passados, compostos por eventos ocorridos na linha do tempo, oferecendo-as como um cache durável de dados. Pode-se afirmar que aplicações baseadas em CQRS e event-sourcing são eventualmente consistentes.

Domain model: trata-se da representação conceitual do domínio e muitas vezes trata-se de algo complexo. Em uma abordagem DDD, em conjunto com CQRS, o domínio estaria posicionado no modelo de Escrita. Separar modelos e repositórios nos obriga a pensar sobre a sincronização entre eles, podendo ser realizada através de diversos mecanismos como agendamento periódico, processos síncronos ou sob demanda por parte do modelo de Leitura. Para manter a sincronização de ambos os modelos, Escrita e Leitura, a consistência eventual dos dados através de um processo assíncrono poderia ser aplicada. Importante observar que a atualização eventual, absorvida implicitamente ao adotarmos CQRS, parte do princípio de que toda a informação possui grande chance de estar defasada.

Quando usado CQRS existem dois tipos de mensagens usadas para a troca de informações dentro da aplicação, comandos e eventos. Comandos são verbos que se propõem a alterar o estado de um componente, utilizando uma abordagem domain-driven design, poderia tratar-se de agregados.

Microsserviços orientados a evento (EDA) seguindo o padrão CQRS (command query responsibility segregation) podem ser alcançados naturalmente. CQRS possibilita, em conjunto com design patterns como event-sourcing, com que os eventos ocorridos em uma escala temporal consigam representar um resultado agregado cujo sua composição seja a sequência de eventos ocorridos (event-sourcing), facilitando a auditoria dos dados. Utilizando CQRS separamos nossa estrutura em dois modelos de serviço separados, modelo de leitura (read model) e modelo de escrita (write model), dividindo a responsabilidade.

CQRS com DDD (Domain Driven Design)

CQRS possui a noção de comandos. O conceito de commands representa uma intenção de negócio, descrita de forma imperativa e disparada no formato de eventos, sendo controlados e interpretados pelos respectivos handlers. Comandos podem gerar novos eventos de sucesso ou falha a partir de seu processamento, permitindo a reação por parte dos event handlers, dando prosseguimento a cadeia.

Comandos expressam intenções de alteração de estado nos objetos através de ações. Comandos capturam intenções, a integridade de determinados comandos está relacionada ao estado de outros comandos que alteram o estado de componentes externos. Comandos servem para possibilitar descrevermos comportamentos com o nível apropriado de granularidade quando representando a intenção do usuário em modificar os dados. Comandos são um dos elementos principais do CQRS, pois obriga-nos a repensar nossa interface de usuário (UI), permitindo capturar com mais assertividade as intenções de nossos usuários.

Comandos devem possuir seu target apontando para somente uma entidade, devendo ser inteiramente processados por um aggregate root (DDD), onde possa ser garantida sua própria consistência eventual ao mesmo tempo em que a integridade das regras de negócio validadas. Comandos devem possuir como boa prática o uso de IDs (identificadores das linhas as quais eles irão afetar).

A forma como os comandos serão processados em baixo nível diz respeito aos detalhes de implementação do padrão CQRS que você escolheu. Sempre que um comando altera o estado da aplicação, um processo precisa disparar eventos refletindo sua alteração, para que o modelo responsável pela modificação possa refletir a mudança de estado. O modelo de escrita recebe tarefas (comandos), aplica adequações, e informa o modelo de leitura que novas informações estão disponíveis.

Comandos devem ser validados no servidor, garantindo segurança e integridade, não confiando apenas em validações do lado cliente, caso existam.

Comandos são disparados como ações executadas pelo usuário de uma interface UI, roteados para que seu comportamento seja aplicado a um componente, alterando seu estado. Comandos não devem ser centrados nos dados e sim definir tarefas, devendo ser colocados em fila para processamento assíncrono.

Comandos são intenções de ações, e mudam o estado do sistema, podendo ser processados por somente um recipiente (target). Comandos tipicamente são enviados dentro de um bounded context (DDD) com target para algum Agregado (DDD) que o processará utilizando um @CommandHandler.

Por outro lado, eventos (events) são notificações de algo ocorrido, uma alteração de estado, e pode interessar a diversos Subscribers, os quais podem estar inscritos (broadcasting). Eventos podem ter Subscribers tanto dentro do bounded context onde ele foi publicado, quanto dentro de outros bounded contexts pertencentes a domínios distintos. A consistência transacional, relacionada a integridade de comandos e eventos, deve contar com filas ou tópicos transacionais com garantia de entrega de mensagens. Desta forma, particularmente possuindo certa atomicidade. Eventos representam o tempo verbal passado em relação ao comando que originou sua criação.

Seguindo DDD (domain-driven design), teríamos a application layer (camada de aplicação) orquestrando a comunicação entre os modelos de escrita (command, domain model) e leitura (queries e view models).

No modelo de leitura, projeções de queries e viewmodels, semelhantes aos snapshots podem ser utilizadas. Dependendo da quantidade de eventos que uma aplicação recebe, determinadas projeções podem ser recriadas ao reprocessarmos todos os eventos desde o último snapshot. Ao mesmo tempo em que projeções inteiras podem ser atualizadas sem impactar o modelo de escrita caso seja necessário. O uso de consultas flat em bancos de dados desnormalizados no modelo de leitura evita o uso de joins, aumentando a performance. No CQRS, consultas nunca devem modificar a base de dados, devendo retornar um DTO (data transfer object) que não expõe quaisquer características do domínio.

Considerações Finais

Nos cenários distribuídos dos microsserviços, diversas réplicas somente leitura podem ser utilizadas visando escalar horizontalmente melhorando o desempenho de nossas aplicações. Adicionado a possibilidade de segregação de repositórios de leitura e escrita, seria possível redimensionar os modelos separadamente respondendo mais rapidamente mudanças de carga de processamento.

CQRS pode melhorar questões de segurança, permitindo isolar o canal de atualizações dos componentes em um só lugar, como agregados no modelo de escrita, Repositórios individuais, barramentos de comandos e eventos (command e eventbus), write data store, read data store.

A implementação prática do CQRS pode ser realizada de diversas maneiras, modelos em memória, compartilhando a mesma base de dados, ou bases diferentes. Modelos tradicionais armazenam apenas o último estado de nossos objetos e na maior parte das vezes é o suficiente, porém em alguns casos precisamos saber qual foi o caminho que os dados trilharam para chegar em determinado resultado.

Ao adotarmos modelos distintos, a intenção de alterações nos dados é preservada pelo modelo de escrita ao mesmo tempo em que a recompilação das views de leitura se torna mais simples. O tratamento de inconsistências pode ser realizado com event-sourcing simplesmente olhando para o passado e recriando as instâncias dos objetos, facilitando o troubleshooting (resolução de problemas).

Utilizar uma arquitetura orientada a eventos (EDA) traz diversos benefícios na adoção de CQRS, podendo variar caso a caso, mas certamente sendo uma base natural para a aplicação de padrões de arquitetura com natureza semelhante ao CQRS. Precisamos ter cuidado, pois se por um lado uma arquitetura orientada a eventos (EDA) pode trazer benefícios, como o uso do padrão event-sourcing em conjunto com CQRS e DDD, por outro pode aumentar a complexidade de nossa aplicação tanto em implementação quanto em manutenção. Sendo mais indicado, que o caminho natural de adoção seja CQRS ou event-sourcing em um ecossistema com a existência do EDA (event-driven architecture) e não o oposto.

Quando utilizamos CQRS, as complexidades de implementação podem residir em ambos os modelos, write model (modelo de escrita) e read model (modelo de leitura). Podendo algumas empresas possuírem o read model mais sofisticado ao mesmo tempo que podem haver outras cujo o write model possua mais complexidades. Independente do nível de dificuldade e das particularidades de cada modelo (escrita e leitura), a evolução dos sistemas em termos de frequentes alterações nas regras de negócio, refletidas no modelo de Escrita, isolam o modelo de leitura sem o comprometer.

CQRS separa estes conceitos sustentando seus pilares um poderoso design pattern. Além disso, nos fazer enxergar se estamos no caminho certo em termos de limites transacionais dos componentes dentro de nossos bounded contexts (contextos delimitados), os quais deveriam possuir responsabilidades mínimas e um nível de isolamento transacional adequado.

CQRS não necessariamente deve ser adotado em modernas aplicações que utilizam ES (event-sourcing) e DDD (domain-driven design), podendo ser implementado em qualquer aplicação a qual lê e escreve dados. Neste caso, não existindo restrições e nem dependências com outros padrões arquiteturais.

CQRS apresenta um padrão arquitetural apropriado para aplicações colaborativas multiusuário, considerando fatores como volatilidade e escalabilidade. CQRS seria mais sobre explorar formas mais simples para construção de softwares complexos, nos fazendo olhar diferentemente para os sistemas distribuídos (arquitetura de microsserviços). O CQRS somente será aproveitado em sua plenitude se considerarmos, desde a concepção de nossos projetos, a interface de usuário e a captura de intenções e comportamentos explícitos (task-based UI) relacionados ao domínio de negócio.

--

--