Marcio Cunha

Modelagem de Domínios Complexos com Event Sourcing e Projeções de Leitura Assíncronas

Descubra como estruturar sistemas corporativos complexos usando Event Sourcing para guardar o histórico exato de cada alteração e projeções assíncronas para consultas rápidas.

Marcio Cunha•6 min
Também disponível em:EnglishEspañol
Resumo
  • O armazenamento baseado em eventos preserva o histórico imutável de todas as operações realizadas no sistema.
  • A separação estrita entre escrita e leitura evita contenção de recursos em grandes volumes de dados.
  • Projeções assíncronas atualizam tabelas otimizadas para consulta sem bloquear o fluxo principal de gravação.
  • A eventual consistency exige estratégias de design para lidar com atrasos milissegundos na interface.
  • A reconstrução do estado atual a partir do zero facilita auditorias e investigações de falhas.

O Desafio de Mapear Regras de Negócio em Sistemas Modernos

Quando construímos softwares para resolver problemas do mundo real, frequentemente enfrentamos a dor de comprimir fluxos complexos em tabelas relacionais tradicionais. Em uma aplicação convencional, salvamos apenas a foto atual do estado de um registro, sobrescrevendo dados antigos. Na prática, isso significa que perdemos a narrativa temporal, o contexto que levou a determinada decisão e a capacidade de auditar o passado com precisão. Para domínios onde cada transação importa — como finanças, logística e saúde —, essa perda de histórico cria um abismo entre o código e a realidade do negócio.

A modelagem rica de domínios exige que o software fale a mesma linguagem dos especialistas da área. Quando o modelo de dados é construído apenas para economizar espaço em disco, ele falha em refletir comportamentos vitais. Em vez de perguntar apenas qual é o saldo atual de uma conta, o sistema deveria compreender a sequência exata de depósitos e saques que geraram esse saldo. É justamente nesse cenário de alta complexidade que abordagens orientadas a eventos ganham tração, transformando a forma como pensamos sobre persistência e consistência de dados em larga escala.

Entendendo Event Sourcing na Prática

Event Sourcing, ou armazenamento de eventos, é uma estratégia de arquitetura onde não salvamos o estado final de um objeto, mas sim a lista cronológica de todos os fatos que aconteceram com ele. Pense em um extrato bancário: você não tem apenas o número 'R$ 1.500' guardado em uma coluna; você tem uma lista de eventos como 'ContaCriada', 'DepositoRealizado de R$ 2.000' e 'SaqueRealizado de R$ 500'. O estado atual não passa de uma consequência matemática derivada da reprodução sequencial desses eventos desde o início dos tempos.

Na prática, isso significa que o banco de dados se torna um log de inserção exclusiva, onde registros nunca são atualizados ou apagados. Essa característica traz uma vantagem imensa de concorrência, pois gravações em logs sequenciais geram pouquíssima contenção em comparação a atualizações de linhas em tabelas convencionais. Além disso, se descobrirmos um bug em uma regra de negócio antiga, podemos corrigir o código e reprocessar os eventos para recalcular o estado de forma retroativa, garantindo uma rastreabilidade sem precedentes para o software.

Para ilustrar como tratamos esses eventos no código, veja um exemplo em C# mostrando a estrutura básica de um agregador aplicando um evento de negócio:

public class ContaBancariaAggregate : AggregateRoot
{
    private decimal _saldo;
    private bool _ativa;

    public void Aplicar(ContaCriadaEvent evento)
    {
        _ativa = true;
        _saldo = evento.SaldoInicial;
    }

    public void Aplicar(DinheiroDepositadoEvent evento)
    {
        if (!_ativa) throw new InvalidOperationException("Conta inativa.");
        _saldo += evento.Valor;
    }
}

O Papel Crucial das Projeções de Leitura Assíncronas

Guardar apenas eventos traz um desafio operacional evidente: como exibir a lista de clientes ou o saldo consolidado de forma rápida na tela do usuário? Se tivéssemos que ler todo o histórico de eventos de uma conta toda vez que alguém abrisse o aplicativo, o sistema ficaria insuportavelmente lento. É aqui que entram as projeções de leitura assíncronas, que funcionam como ouvintes dedicados a traduzir o fluxo contínuo de eventos em tabelas otimizadas para consulta.

Na prática, esses projetores escutam o barramento de eventos em segundo plano, calculam o estado consolidado e gravam o resultado em um banco de dados secundário, como uma base relacional comum ou um documento NoSQL. Esse conceito se alinha ao padrão CQRS, que separa a responsabilidade de escrita da responsabilidade de leitura. Enquanto a escrita foca na validação rigorosa das regras de negócio e no armazenamento seguro dos fatos, a leitura foca puramente na performance e na usabilidade da interface gráfica.

Como esse processo ocorre fora da requisição principal do usuário, ele é assíncrono. Isso introduz o conceito de consistência eventual, o que significa que pode haver um atraso de frações de segundo entre o momento em que um evento é gravado e o momento em que ele aparece refletido nas telas de consulta. Para a imensa maioria das aplicações corporativas, esse trade-off é amplamente compensado pela escalabilidade e pela robustez estrutural alcançadas.

Gerenciamento de Concorrência e Confiabilidade Distribuída

Quando removemos o bloqueio tradicional de banco de dados e adotamos arquiteturas baseadas em eventos, precisamos lidar com a concorrência de outra maneira. Se dois usuários tentarem modificar o mesmo agregado exatamente ao mesmo tempo, como evitamos que um sobrescreva o trabalho do outro de forma silenciosa? A resposta reside no uso de controle de concorrência otimista através de números de versão em cada fluxo de eventos.

Na prática, cada novo lote de eventos gerado por um comando carrega o número da versão esperada do agregado. Quando o mecanismo de persistência tenta salvar esses eventos, ele verifica se a versão no banco de dados corresponde à versão enviada. Se outro processo gravou algo nesse meio-tempo, a versão muda, a transação é rejeitada por conflito, e a aplicação cliente é informada para tentar novamente com os dados atualizados. Esse modelo elimina gargalos de bloqueio pessimista, permitindo que milhares de operações ocorram simultaneamente sem travar conexões.

Abaixo, um exemplo em Node.js demonstrando como um manipulador de eventos atualiza uma projeção de leitura em um banco relacional leve:

async function handleDinheiroDepositado(evento, dbPool) {
    const query = `UPDATE saldos_clientes SET saldo = saldo + $1, atualizado_em = NOW() WHERE cliente_id = $2`;
    await dbPool.query(query, [evento.valor, evento.clienteId]);
}

Esse tipo de rotina garante que o modelo de leitura permaneça sincronizado com o barramento de mensagens, operando de forma desacoplada e resiliente a quedas momentâneas de microsserviços dependentes.

Estratégias para Migração e Evolução de Esquemas

Manter um sistema baseado em eventos por vários anos traz um desafio único: o que acontece quando a estrutura de um evento precisa mudar? Em bancos de dados relacionais tradicionais, alteramos uma coluna com um comando de migração e o problema está resolvido. No Event Sourcing, os eventos armazenados no passado são imutáveis; você não pode simplesmente reescrever o histórico de transações que ocorreram em produção há três anos.

Para resolver esse dilema, utilizamos estratégias de versionamento e upcasting. O upcasting consiste em interceptar eventos antigos no momento em que são lidos do armazenamento e transformá-los dinamicamente para a estrutura da versão mais recente antes de passá-los para o domínio. Desta forma, o núcleo da aplicação lida apenas com a versão atualizada dos objetos, enquanto o arquivo histórico permanece intacto e fiel ao que realmente aconteceu no momento da emissão original.

Outro ponto crítico é a limpeza e o snapshotting. Como o histórico de um agregado pode crescer indefinidamente, reprocessar milhares de eventos a cada carregamento de objeto pode degradar a performance. A solução é criar instantâneos periódicos do estado consolidado, permitindo que a aplicação leia o último snapshot e processe apenas os poucos eventos gerados após ele, unindo o melhor dos dois mundos: auditoria completa e alta velocidade de execução. Para complementar essa implementação em produção, vale conferir também nossas análises técnicas sobre Modelagem de Domínios com Event Sourcing e Projeções de Leitura e Modelagem de Domínios com Event Sourcing e Projeções CQRS Assíncronas em NoSQL.

Considerações Finais sobre Arquiteturas Orientadas a Eventos

Adotar modelagem de domínios complexos com Event Sourcing e projeções de leitura assíncronas exige maturidade técnica e clareza sobre os problemas que você está tentando resolver. Não se trata de uma bala de prata aplicável a qualquer CRUD simples, mas sim de uma ferramenta poderosa para domínios ricos onde a rastreabilidade, a auditoria rigorosa e a escalabilidade de leitura e escrita justificam o esforço inicial de implementação.

Ao separar a complexidade transacional do armazenamento de fatos da agilidade das projeções de leitura, construímos sistemas altamente resilientes, capazes de evoluir junto com as demandas do negócio sem corromper o passado. O investimento em dominar esses padrões arquiteturais paga dividendos duradouros na forma de softwares mais transparentes, fáceis de debugar e preparados para crescer sem fricção.