Implementando o Padrão CQRS com Python e FastAPI
O propósito deste post é exemplificar o desenvolvimento e a aplicação do padrão arquitetônico CQRS, por meio da utilização da linguagem Python com o framework FastAPI.
- Não entraremos em profundidade em todos os padrões citados neste post.
- Se você já conhece o padrão e deseja ir diretamente a minha implementação de código, click aqui.
- Este artigo será uma variação do serviço construído no meu artigo anterior — Migrando sistemas monoliticos para microsserviços com Domain Driven Design. (minha implementação)
Oque é CQRS ?
CQRS é a sigla para “Command Query Responsibility Segregation”, ou em português, “Segregação de Responsabilidade de Comando e Consulta”. Trata-se de um padrão de arquitetura de software que consiste na separação da lógica de escrita (comandos) da lógica de leitura (consultas) em modelos distintos.
O CQRS é o resultado da promoção de um rigoroso principio de projeto de objeto (Ou componente), separação de comandos e consultas (CQS) em um padrão arquitetônico.
Este principio foi criado por Bertrand Meyer, e afirma o seguinte:
Cada método deve ser um comando que realiza uma ação ou uma consulta que retorna dados para o chamador, mas não ambos. Em outras palavras, fazer uma pergunta não deve alterar a resposta. Mais formalmente, os métodos devem retornar um valor apenas se forem refereêncialmente transparentes e, portando, não posuirem efeitos colatereais.
Em um nível de objeto isso significa:
- Se um método modificar o estado de um objeto, ele é um comando, e seu método não deve retornar um valor. E algumas linguagens de programação significa um método void.
- Se um método retorna algum valor, ele é uma consulta, e não deve direta ou indiretamente provocar alguma modificação no estado do objeto.
referência: VAUGHN VERNON. IMPLEMENTANDO DOMAIN-DRIVEN DESIGN. Página 139. Capitulo 4
As vezes pode ser difícil consultar a partir de Repositórios todos os dados que os usuários precisam visualizar. Isto se torna ainda mais difícil trabalhando com objetos agregados, que quanto mais sofisticado nosso domínio, mais situações de complexidade aparecerão.
Utilizar apenas repositórios para resolver isso pode ser menos desejável, podemos exigir que os clientes usem múltiplos Repositórios para obter todas as instâncias de agregado necessárias.
Entendendo Alguns Cenários
Após a breve introdução sobre CQRS, vamos explorar alguns cenários comuns em aplicações. Geralmente, essas aplicações possuem seu próprio banco de dados, o que garante integridade e consistência dos dados. Contudo, se a aplicação estiver em um cenário com alta demanda de consumo de dados e operações intensivas de leitura e escrita, é possível optar pelo padrão CQRS.
Imagine ter que realizar uma consulta em um banco de dados SQL que envolve relacionar diversos modelos diferentes para obter o agregado desejado. Certamente, isso não seria agradável para a aplicação. Por outro lado, com o uso de bases distintas, é possível obter o melhor para a escrita e a leitura da aplicação. A base de comando pode ser relacional, separando cada modelo para gravação, enquanto a base de leitura pode ser uma base NoSQL trabalhando com objetos agregados, facilitando a consulta.
Em um cenário como o descrito acima, é possível persistir o objeto agregado após a execução do comando, por meio de um cache. Dessa forma, a consulta seria ainda mais facilitada e possíveis problemas de inconsistência nos dados seriam evitadas.
Ao utilizar uma camada de persist, é possível trabalhar de forma assíncrona na gravação de dados, ao invés de tentar manter todos os dados consistentes em tempo real. Além disso, quando combinado com o event sourcing, podemos alcançar uma consistência ainda maior no armazenamento de dados. A cada comando que é executado, uma nova versão do objeto agregado é criada, o que permite que possamos acompanhar as mudanças de estado do objeto ao longo do tempo.
Ao utilizar essa abordagem, é possível reconstruir o estado atual do objeto agregado a partir do log de eventos gerado pelo event sourcing, bem como recuperar estados anteriores. Isso permite garantir a consistência eventual dos dados, mesmo em cenários com alta demanda de leitura e escrita.
Por onde começamos a implementação?
A implementação do CQRS pode ser iniciada de diversas formas, dependendo dos requisitos e da arquitetura da aplicação. No entanto, como sugestão, uma boa forma de começar é pela construção do mediador e do componente abstrato, que permitirão a implementação dos modelos de consulta e comando.
O mediador é responsável por receber e direcionar as solicitações de comandos e consultas para os modelos apropriados. Já o componente abstrato é uma interface que define os métodos que os modelos de comando e consulta devem implementar. Dessa forma, é possível garantir a consistência e separação de responsabilidades entre os modelos.
Camada Mediadora
A camada mediadora é uma parte importante da arquitetura CQRS e é implementada por meio do padrão de design comportamental conhecido como Mediator. Esse padrão tem como objetivo reduzir dependências caóticas entre objetos, limitando as comunicações diretas entre eles e forçando-os a colaborar apenas por meio de um objeto mediador.
O padrão Mediador sugere que, em vez de permitir a comunicação direta entre os componentes da aplicação, eles devem colaborar indiretamente por meio de um objeto mediador especial. Esse objeto é responsável por receber e direcionar as chamadas para os componentes apropriados, permitindo que esses componentes sejam independentes e tenham baixo acoplamento.
Analogia ao mundo real
Os pilotos de aeronaves não conversam diretamente entre si ao decidir quem será o próximo a pousar o avião. Toda a comunicação passa pela torre de controle.
Click aqui para saber mais sobre o padrão Mediator,
Seguindo a sugestão apresentada no diagrama UML, iremos definir a nossa Interface Mediadora e um Componente Abstrato para a camada mediadora. A Interface Mediadora irá declarar métodos de comunicação com os componentes de Comando e Consulta. É importante ressaltar que os componentes podem passar qualquer contexto como argumentos desse método, incluindo seus próprios objetos. No entanto, é necessário evitar que haja acoplamento entre um componente receptor e a classe do remetente.
Nossos componentes serão classes de Comando e Consulta que conterão a lógica de negócio. Cada componente terá uma referência ao Mediador, que será declarado com o tipo da Interface do Mediador. Dessa forma, o componente não precisa estar ciente da classe real do mediador, reduzindo o acoplamento entre eles.
Definindo Interfaces de Comando e Consulta
A Interface de Comando não deve estar ciente do componente de Consulta. Caso algo importante aconteça dentro ou para um componente, ele deve notificar somente o mediador. Quando o mediador recebe alguma notificação, ele pode facilmente identificar o remetente, o que pode ser suficiente para decidir qual componente deve ser acionado em retorno.
Para a Interface de Comando, trabalharemos com a herança do Componente Abstrato e declararemos os métodos de comunicação relacionados ao modelo de Comando.
Assim como na Interface de Comando, na Interface de Consulta também devemos evitar que o componente esteja ciente do mediador ou dos outros componentes. Portanto, vamos definir a Interface de Consulta declarando apenas seus métodos de comunicação com o mediador, que será responsável por redirecionar a consulta para o componente adequado. É importante lembrar que as consultas não devem alterar o estado do objeto agregado, pois essa responsabilidade é exclusiva dos comandos.
Ao definir o modelo de comando concreto, é importante ter em mente que ele é responsável por executar comportamentos e, às vezes, realizar gravações no objeto agregado. Cada método presente no modelo de comando deve concluir a execução publicando um evento que expresse a mudança de estado do objeto agregado. É importante ter cuidado ao definir as operações do modelo de comando, uma vez que ele é responsável por garantir a consistência do objeto agregado.
Também a situações que o envio de comandos não levam à publicação de eventos. Por exemplo, se um comando foi entregue por um sistema de mensagem do tipo "pelo menos uma vez" e a aplicação garante operações indepotentes, a mensagem re-entregue é silenciosamente descartada.
Em nosso modelo de Comando, seguiremos a abstração da Interface de Comando e implementaremos os métodos de comunicação de forma simples e direta.
Repare na aparição do nosso mediador na linha 54, introduzido o seu recurso de persistência para nosso objeto agregado.
Definindo o Modelo de Consulta Concreto
O Modelo de Consulta é um modelo de dados não normalizados. Sua finalidade é fornecer apenas dados para exibição e possivelmente relatórios, sem oferecer comportamento para o domínio.
Para implementar o nosso Modelo de Consulta, novamente utilizaremos uma interface, neste caso a de Consulta.
Repare novamente a aparição de nosso mediador, neste caso trabalhando sobre os dados persistidos, para evitar uma inconsistência em nossos dados.
Implementando nosso Mediador.
Nosso Mediador irá encapsular as relações entre os componentes de Comando e Consulta, mantendo o contexto de todos os componentes que ele gerencia.
A implementação deste Mediador é relativamente simples, pois sua responsabilidade é acionar os métodos de comando e consulta. Mesmo que a implementação seja simples, é importante lembrar que o padrão de design do Mediador é fundamental para reduzir dependências caóticas entre objetos, tornando a aplicação mais fácil de entender, manter e evoluir.
Event Sourcing
No contexto do padrão CQRS, a prática do Event Sourcing desempenha um papel importante, promovendo uma abordagem em que cada modificação no estado de uma aplicação é registrada como um evento. Esses eventos são, então, armazenados sequencialmente, criando um histórico completo das mudanças ao longo do tempo.
O OrderEventStoreRepository
é especialmente projetado para lidar com essa abordagem. Ele se encarrega do armazenamento e recuperação dos eventos de domínio relacionados a pedidos. Cada evento registrado através do repositório é uma representação significativa de uma transição de estado no ciclo de vida de um pedido, proporcionando uma visão detalhada e auditável da evolução desse pedido ao longo do tempo.
Injetando nossas dependências
A implementação do CQRS fica claramente visível quando fazemos a injeção de dependências, pois é possível observar facilmente a criação dos componentes de Comando, Consulta e Mediador que serão utilizados pela nossa camada intermediária. Isso torna mais fácil o gerenciamento e a manutenção de nossos componentes, garantindo uma melhor organização e coesão do código.
Concluindo
Em resumo, o padrão CQRS (Command Query Responsibility Segregation) permite separar a lógica de negócio da camada de entrega, o que torna a manutenção mais fácil e reduz as dependências. Com ele, é possível obter diversos benefícios, como:
- Escalabilidade: as operações de leitura e gravação podem ser dimensionadas de forma independente, o que significa que as aplicações com muita leitura podem ser dimensionadas facilmente sem afetar as operações de gravação e vice-versa.
- Performance: ao otimizar cada modelo para seu caso de uso específico, o CQRS pode levar a melhorias significativas de desempenho. O modelo Query pode ser projetado para recuperação rápida de dados, enquanto o modelo Command pode ser otimizado para consistência e durabilidade.
- Separação de Preocupações: o CQRS fornece uma clara separação de preocupações, tornando a base de código mais fácil de entender e manter. Os desenvolvedores podem raciocinar mais facilmente sobre como as alterações na base de código afetarão o sistema geral.
- Flexibilidade: o CQRS permite que cada modelo evolua de forma independente, uma vez que não estão fortemente acoplados. Isso significa que alterações em um modelo podem ser feitas sem afetar o outro modelo.
Porém, também há desvantagens em se adotar o CQRS, tais como:
- Maior Complexidade: o CQRS pode aumentar a complexidade da aplicação, uma vez que existem agora dois modelos distintos a manter. Isso pode levar a uma curva de aprendizado mais acentuada para os desenvolvedores e a uma base de código mais complexa em geral.
- Inconsistência Eventual: como os modelos Comando e Consulta são separados, pode haver um atraso entre a gravação dos dados e a disponibilização para leitura. Esse atraso é conhecido como consistência eventual e deve ser considerado no design do aplicativo.
- Duplicação de Dados: como os modelos Comando e Consulta são separados, pode haver alguma duplicação de dados entre eles. Isso pode levar a maiores requisitos de armazenamento e a exigir esforço adicional para manter os dados sincronizados e consistentes.
Tecnologias e padrões utilizados:
- Python 3.9: Uma linguagem de programação interpretada e de alto nível, com uma sintaxe limpa e fácil de aprender, amplamente utilizada em uma ampla gama de aplicações.
- FastAPI (API Rest): Um framework para criação de APIs RESTful em Python que é rápido, fácil de usar e altamente escalável.
- ElasticSearch: um mecanismo de pesquisa e análise de dados distribuído que permite armazenar, pesquisar e analisar grandes volumes de dados em tempo real.
- Redis: Um banco de dados em memória que suporta diversas estruturas de dados, incluindo strings, hashes, listas, conjuntos e mapas ordenados.
- CQRS: Uma abordagem de arquitetura de software que separa as operações de leitura e escrita em modelos distintos.
- Ports & Adapters: Um padrão arquitetural que separa a lógica de negócios do aplicativo de suas dependências externas, permitindo que as mesmas sejam facilmente substituídas ou atualizadas.
- Event Sourcing: um padrão arquitetural que consiste em armazenar todas as alterações no estado de um sistema como uma sequência de eventos imutáveis, em vez de apenas o estado atual.
Referências
- https://sourcemaking.com/design_patterns/mediator
- https://microservices.io/patterns/data/cqrs.html
- https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/
- https://www.cosmicpython.com/book/chapter_12_cqrs.html
- https://github.com/marcosvs98/cqrs-architecture-with-python
- https://www.linkedin.com/feed/update/urn:li:activity:7034107532293849088/