Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

03 - Modelos de Leitura e Escrita distintos

#desnormalizacao#materialized-view#projection#read-model
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

Modelos de Leitura e Escrita Distintos

Quando os lados de comando e consulta se separam, os modelos de dados também precisam se separar, porque servem a fins opostos. O modelo de escrita existe para proteger invariantes: é normalizado, centrado no domínio e tolera joins porque só o lado de comando o consulta. Os modelos de leitura existem para responder perguntas rápido: são desnormalizados, moldados por tela e por relatório, e duplicam dados sem culpa porque nada ali decide comportamento. As projeções são a mecânica que mantém essa família de visões atualizada, transformando eventos de escrita em linhas, documentos e índices de leitura. Em produção, o desenho desses modelos define a experiência do usuário, o custo da infraestrutura e a facilidade de evoluir consultas, e é sobre como projetá-los, materializá-los e reconstruí-los que este arquivo trata.

Por que os dois lados pedem modelos diferentes

O modelo de escrita responde à pergunta: como garantir que uma regra não seja violada? A resposta pede estrutura próxima ao domínio, com agregados coerentes, chaves estáveis e o mínimo de redundância, para que cada mudança toque um único lugar. O modelo de leitura responde à pergunta: como devolver esta resposta em milissegundos? A resposta pede exatamente o oposto, dados pré-combinados na forma em que serão exibidos, sem joins no caminho quente e com índices alinhados aos filtros reais.

Tentar atender às duas perguntas com um único modelo produz os dois piores resultados ao mesmo tempo. A escrita ganha índices que só servem a relatório, transações mais longas e deadlock novo a cada tela nova; a leitura ganha queries com seis joins que degradam em cada pico. A separação existe para que cada pergunta tenha a estrutura que merece, e a duplicação de dados entre os modelos é um custo aceito conscientemente, porque a coerência é garantida pela sincronização, e não pela normalização.

A fronteira também muda o ciclo de vida dos modelos. O modelo de escrita muda devagar, porque toda alteração toca regras, agregados e migração de dados históricos. Os modelos de leitura mudam rápido e morrem com frequência, porque telas e relatórios mudam com o negócio. Reconhecer essa diferença de ritmo é libertador: a equipe pode adicionar, ajustar e aposentar visões sem tocar no coração do sistema, e é justamente essa plasticidade que o acoplamento em modelo único impede.

Projections: transformar eventos em visões

A projeção é um consumidor de eventos que mantém uma visão de leitura. Ela assina o fluxo, mantém um checkpoint da posição global processada e aplica uma função de transformação: cada evento entra, linhas do read model mudam. A visão é, em última instância, um replay materializado e incremental do log, e herda dele a capacidade de ser reconstruída do zero a qualquer momento.

O desenho de uma projeção começa pela pergunta que ela serve, e não pelos eventos. Listar os pedidos do cliente com status e total pergunta por uma tabela plana com uma linha por pedido; rastrear itens pede uma segunda visão com uma linha por item; o painel do backoffice pede agregações por dia. Três perguntas, três visões, uma única sequência de eventos consumida por três projetores independentes, cada um com seu checkpoint e seu ritmo.

Cada projetor é um processo pequeno, com responsabilidade única: consumir, transformar, gravar e avançar o checkpoint. Falha em um projetor não derruba os demais, e a visão afetada simplesmente atrasa, com o lag visível em métrica. Essa isolação é uma das vantagens silenciosas do desenho: o acidente de uma visão tem raio limitado, e a recuperação é reprocessar o log a partir do checkpoint, sem envolver o resto do sistema.

A mesma disciplina do modelo de escrita não se aplica aqui, e isso é deliberado. Visões podem duplicar o nome do cliente em cada linha de pedido, podem somar totais incrementalmente e podem manter contadores separados. O critério de qualidade é outro: a visão responde a pergunta dela com uma leitura, sem joins e sem cálculo em tempo real?

Desnormalização deliberada

A desnormalização no read model é uma decisão de engenharia, e não um descuido. Cada campo duplicado compra uma leitura mais rápida e paga um pequeno custo na projeção. O exemplo típico de e-commerce mostra a diferença na forma do dado.

// Projeção: mantém a visão resumida usada pela tela Meus Pedidos
public class PedidosResumoProjection {

    private final PedidosResumoDao dao;

    public PedidosResumoProjection(PedidosResumoDao dao) {
        this.dao = dao;
    }

    public void processar(EventoEnvolto evento) {
        switch (evento.tipo()) {
            case "PedidoCriado" -> dao.inserirResumo(
                evento.dados().getString("pedidoId"),
                evento.dados().getString("clienteId"),
                evento.dados().getString("nomeCliente"),   // nome duplicado de propósito
                "AGUARDANDO_PAGAMENTO", BigDecimal.ZERO, 0);

            case "ItemAdicionado" -> dao.acumularItem(
                evento.dados().getString("pedidoId"),
                evento.dados().getBigDecimal("valorLinha"),
                evento.dados().getInt("quantidade"));

            case "PedidoPago" -> dao.atualizarStatus(
                evento.dados().getString("pedidoId"), "PAGO");
        }
        dao.marcarCheckpoint(evento.posicaoGlobal());   // progresso sobrevive a reinícios
    }
}

O nome do cliente duplicado evita o join com a tabela de clientes em cada listagem; os totais acumulados evitam somar itens a cada resposta. Quando o cliente troca de nome, um evento de domínio correspondente atualiza as visões que carregam o dado, e o custo é o de atualizar linhas, não o de reescrever regras. A pergunta que orienta cada duplicação é sempre a mesma: quem consulta este campo, com que frequência, e quanto custa mantê-lo atualizado na projeção versus calcular na consulta.

O SQL da visão resultante mostra o ganho na leitura.

-- Tela Meus Pedidos: uma linha por pedido, sem nenhum join
SELECT pedido_id, nome_cliente, status, total, quantidade_itens
  FROM pedidos_resumo
 WHERE cliente_id = $1
 ORDER BY criado_em DESC
 LIMIT 20;

Uma consulta de um índice, resposta em milissegundos mesmo com milhões de pedidos. É esse contraste com a listagem de seis joins do modelo único que justifica a duplicação, e a mesma lógica se aplica a cada campo duplicado em cada visão.

Materialized views: quando o banco resolve

Nem toda visão precisa de projetor dedicado. Bancos relacionais oferecem views materializadas, que persistem o resultado de uma query e o refrescam por comando, e esse recurso atende bem visões analíticas de atualização tolerante. Em sistemas com eventos no próprio banco, a view materializada é a forma mais barata de materializar uma agregação, porque o banco cuida da mecânica.

-- View materializada: faturamento por dia a partir dos eventos gravados
CREATE MATERIALIZED VIEW vendas_por_dia AS
SELECT date_trunc('day', gravado_em)         AS dia,
       count(*)                              AS pedidos,
       sum((dados->>'valor')::numeric)       AS faturamento
  FROM eventos
 WHERE tipo = 'PedidoPago'
 GROUP BY 1;

-- Refrescamento concorrente: lê a versão antiga enquanto constrói a nova
REFRESH MATERIALIZED VIEW CONCURRENTLY vendas_por_dia;

O limite dessa abordagem é o refrescamento, que reprocessa a consulta inteira e cresce com o volume. Visões pequenas e refrescamentos periódicos funcionam bem; visões que precisam estar frescas a cada segundo pedem projetor dedicado com checkpoint incremental. A regra prática: view materializada para agregação analítica com tolerância de minutos, projeção dedicada para visão operacional com tolerância de segundos.

Reconstrução do read model do zero

A capacidade de reconstruir qualquer visão a partir do log é a propriedade que dá coragem para desnormalizar. Como a visão é derivada, ela pode ser jogada fora e refeita quando a lógica da projeção muda, quando um bug corrompe linhas ou quando um campo novo precisa preencher o histórico. A operação segue o roteiro de estágio e troca: construir a visão nova em tabelas separadas, validar com contagens e somas comparativas, trocar a referência das consultas em um único deploy e descartar a versão antiga depois da janela de reversão.

Durante a reconstrução, a visão em uso continua servindo, e a nova cresce em paralelo, com seu próprio checkpoint partindo da posição zero. Em logs grandes, o reprocessamento usa réplicas de leitura e paralelismo por stream, e a duração esperada é medida e publicada, transformando a reconstrução em operação de rotina. Times que tratam rebuild como procedimento documentado desfrutam do melhor do padrão: corrigir a lógica de leitura sem medo, porque nada no caminho é irrecuperável.

Onde hospedar cada visão

Tecnologia Melhor para Cuidados
PostgreSQL Listagens, telas de operação, visões com filtro e paginação Índices alinhados aos filtros; reconstrução via estágio
Redis Totais quentes, contadores, sessão de carrinho Persistência derivada; aceita perda, pois é reconstrutível
Elasticsearch Busca textual, facetamento, catálogo Mapeamento explícito; sincronização com versionamento de documento
Warehouse Relatórios históricos, BI, agregações longas Custo de ingestão; dados em formato analítico
Em memória no serviço Visões mínimas de altíssima taxa Perda em restart; carga inicial a partir do log

A escolha por pergunta, e não por tecnologia favorita, é o que evita o monocultivo de leitura. Uma aplicação madura costuma manter três ou quatro hospedagens distintas, cada uma servindo a classe de consulta que lhe cabe, todas alimentadas pela mesma sequência de eventos.

O custo operacional desse ecossistema precisa estar no plano desde o início: cada hospedagem traz backup, monitoramento, segurança e ciclo de vida próprios. Times pragmaticos começam com o mínimo que atende as perguntas reais, geralmente um relacional e um cache, e adicionam especializações quando a demanda aparece documentada. O oposto, montar zoo completo antes da primeira consulta real, transforma o read model em um projeto de infraestrutura sem usuário.

Erros comuns e boas práticas

  • Desenhar o read model pela forma do banco de escrita, mantendo joins e normalização; a visão deve nascer da tela ou do relatório, não da tabela.
  • Colocar regras de negócio na projeção, como decidir aprovação; a projeção materializa efeitos de eventos, e decisão é território do comando.
  • Atualizar múltiplas visões dependentes umas das outras; visões devem ser independentes e reconstruíveis sozinhas, sem rede de dependências.
  • Esquecer o checkpoint fora da transação do efeito; o crash duplica ou perde eventos, e a visão diverge silenciosamente.
  • Criar visões sem dono e sem métrica de uso; cada visão tem custo de operação, e visões mortas devem ser desligadas.
  • Assumir que a visão está fresca sem medir lag; publique a defasagem por projeção e alerte quando passar do orçamento.
Sintoma em produção Causa provável Mitigação
Listagem lenta apesar do read model Visão com join herdado do modelo de escrita Redesenhar a visão plana com os campos da tela
Dados divergentes entre visões do mesmo fato Projeções com dependência entre si Isolar projetores e reconstruir a visão errada do log
Visão corrompida sem causa evidente Checkpoint fora da transação do efeito Transacionar efeito e checkpoint com idempotência
Reconstrução estourando janela de manutenção Reprocessamento no primário e sem paralelismo Usar réplica, paralelizar por stream e medir antes