Marcio Cunha

Modelagem de Domínios com Event Sourcing e CQRS em Bancos de Dados Orientados a Eventos

Descubra como construir arquiteturas resilientes utilizando Event Sourcing e projeções CQRS para modelar domínios complexos de negócios e garantir consistência em sistemas distribuídos.

Marcio Cunha•5 min
Também disponível em:EnglishEspañol
Resumo
  • Registrar a intenção do usuário como uma sequência imutável de fatos preserva o histórico exato das operações do sistema sem perdas de auditoria.
  • Separar a escrita da leitura por meio de projeções otimizadas elimina gargalos de performance em consultas de alto volume.
  • Reconstruir o estado atual do sistema através da reprodução contínua de eventos exige estratégias inteligentes de snapshots para evitar lentidão.
  • Garantir a consistência eventual entre o modelo de escrita e as leituras demanda tratamento robusto de mensagens duplicadas e reordenações.
  • Adotar esta abordagem arquitetural compensa o aumento da complexidade operacional apenas em domínios de negócio altamente complexos e dinâmicos.

O Desafio de Mudar a Forma Como Guardamos Dados

Na engenharia de software tradicional, estamos acostumados a olhar para o banco de dados como uma fotografia instantânea. Se um usuário atualiza o endereço de entrega, o sistema sobrescreve a informação antiga pela nova. Na prática, isso significa que perdemos todo o contexto do porquê e de quando a mudança aconteceu. Para domínios de negócios altamente complexos, essa perda de histórico pode custar caro em termos de auditoria, rastreabilidade e resolução de conflitos. É aqui que entra a modelagem baseada em fatos inalteráveis.

Em vez de salvar apenas o estado atual, a abordagem de Event Sourcing (fontes de eventos) consiste em gravar cada alteração de estado como um evento imutável no tempo. Pense nisso como o extrato bancário de uma conta corrente: o saldo não é um número estático guardado numa tabela, mas sim o resultado calculado de todas as entradas e saídas desde a abertura da conta. Quando aplicamos essa lógica ao software, garantimos que o passado do sistema seja tão importante e acessível quanto o presente, abrindo portas para análises retroativas e correção segura de bugs.

A Anatomia de um Evento de Negócio

Para construir um sistema orientado a eventos, precisamos mudar o nosso modelo mental sobre o que é uma entidade de dados. Em vez de classes que guardam propriedades mutáveis, pensamos em fatos que já ocorreram no passado, sempre escritos no particípio passado (por exemplo: PedidoRealizado, PagamentoAprovado ou ItemRemovido). Cada evento carrega consigo um payload, que é o pacote de dados estrito necessário para descrever exatamente o que mudou naquele instante específico do tempo.

Na prática, esses eventos são acumulados em um registro linear, frequentemente chamado de log de eventos ou store de eventos. Nenhum dado é apagado ou atualizado após ser gravado; as operações são estritamente de adição (append-only). Isso traz uma vantagem colossal para a escalabilidade de escrita, já que gravações sequenciais em disco são extremamente rápidas. No entanto, para exibir o estado atual para o usuário na tela, precisaríamos recalcular tudo desde o primeiro dia, o que nos leva diretamente à necessidade de separar as coisas.

Desacoplando Leitura e Escrita com Projeções CQRS

Quando leitor e escritor compartilham a mesma estrutura de dados, começam os problemas de concorrência e gargalos de performance. O padrão CQRS (Command Query Responsibility Segregation, ou separação de responsabilidades entre comandos e consultas) resolve isso dividindo a aplicação em dois caminhos completamente independentes: um lado focado exclusivamente em receber comandos e gerar eventos, e outro lado focado em ler dados já processados.

As projeções são os motores que traduzem os eventos brutos de escrita em tabelas de leitura otimizadas. Na prática, quando um evento do tipo ClienteCadastrado é disparado, um consumidor assíncrono lê esse evento e atualiza um banco relacional ou NoSQL desenhado sob medida para aquela consulta específica na tela do cliente. Se a necessidade de relatórios mudar amanhã, não precisamos alterar a estrutura central do sistema; basta criar uma nova projeção que escute os mesmos eventos e monte uma tabela totalmente nova do zero.

Gerenciando a Complexidade dos Snapshots

Um dos maiores desafios práticos de quem adota a gravação contínua de eventos é o desempenho na inicialização. Se um carrinho de compras acumulou dez mil eventos ao longo de três anos, o sistema precisará ler e reprocessar todos esses dez mil registros toda vez que o usuário quiser ver o carrinho atual. Para evitar gargalos de CPU e memória, utilizamos uma técnica chamada snapshot (captura instantânea).

O snapshot funciona como um ponto de restauração periódico. A cada cem eventos processados, por exemplo, o sistema calcula e salva o estado consolidado da entidade em um registro auxiliar. Quando a aplicação precisa reconstruir o estado atual, ela não lê desde o início dos tempos; ela carrega o último snapshot disponível e processa apenas os eventos gerados após ele. Isso reduz drasticamente o tempo de inicialização e mantém o sistema ágil mesmo sob alto volume operacional.

<!-- Exemplo conceitual de projeção e manipulação de eventos -->

class CarrinhoProjecao: def __init__(self): self.itens = {} self.total = 0.0 def ao_receber_item_adicionado(self, evento): produto_id = evento['produto_id'] quantidade = evento['quantidade'] preco = evento['preco'] self.itens[produto_id] = self.itens.get(produto_id, 0) + quantidade self.total += preco * quantidade def ao_receber_item_removido(self, evento): produto_id = evento['produto_id'] preco = evento['preco'] quantidade = evento['quantidade'] if produto_id in self.itens: self.total -= preco * quantidade del self.itens[produto_id]

Consistência Eventual e Tratamento de Conflitos

Em sistemas distribuídos que separam escrita e leitura, a consistência dos dados deixa de ser imediata e passa a ser eventual. Isso significa que, no exato milissegundo em que um comando é aceito, a tabela de leitura correspondente pode ainda não ter recebido a atualização da projeção. Para o usuário final, isso exige cuidado na interface para evitar a sensação de que o sistema está falhando ou desatualizado.

Além disso, o controle de concorrência otimista torna-se obrigatório. Como vários usuários podem tentar alterar o mesmo recurso simultaneamente com base no mesmo estado passado, o armazenamento de eventos precisa rejeitar gravações caso a versão esperada do agregado não coincida com a versão atual no banco. Essa rejeição força a aplicação a reprocessar a lógica de negócio com os dados mais recentes antes de tentar salvar novamente.

Considerações Finais sobre Arquiteturas Orientadas a Eventos

Adotar modelagem baseada em eventos e projeções separadas não é uma bala de prata. A complexidade operacional de manter um barramento de mensagens, garantir a idempotência dos consumidores e depurar fluxos assíncronos exige maturidade técnica da equipe de engenharia. Contudo, para domínios complexos onde a auditoria estrita, a flexibilidade de relatórios e a escalabilidade granular são requisitos críticos, essa arquitetura oferece uma fundação sólida e duradoura para o crescimento do produto.