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

04 - Eventual Consistency (Consistência Eventual)

#consistencia-eventual#lag#read-your-writes#ux
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

Eventual Consistency (Consistência Eventual)

Consistência eventual é a propriedade de sistemas em que a escrita e a leitura não precisam estar sincronizadas a cada instante, desde que a leitura alcance o estado atual depois de um intervalo. Em CQRS, ela aparece naturalmente quando os lados se separam fisicamente: o comando é confirmado, e a visão de leitura ainda exibe o estado antigo por alguns milissegundos ou segundos, até que a sincronização aconteça. Essa defasagem não é um defeito, é o preço contratado em troca de escala e desacoplamento, e o desenho do sistema precisa assumi-la como premissa. O que separa uma implementação madura de uma ingênua é o tratamento deliberado dos sintomas: garantir que o usuário veja o próprio efeito, comunicar pendência nas telas, medir a defasagem como métrica de primeira classe e saber quando a consistência imediata é realmente exigida pelo negócio.

De onde vem a defasagem

A defasagem nasce na fronteira entre os modelos. O comando grava no lado de escrita e é confirmado; a projeção consome o evento, atualiza a visão e avança o checkpoint. Entre a confirmação do comando e a atualização da visão existe uma janela composta por tempo de publicação do evento, tempo de fila, tempo de processamento e tempo de gravação da visão. Em condições normais, essa janela é de dezenas a centenas de milissegundos; em condições adversas, como picos de tráfego, reprocessamento ou degradação do consumidor, ela pode chegar a minutos.

O ponto central é que a janela é esperada, observável e limitável, e não um acidente. Sistemas que ignoram a defasagem entregam telas que mentem: o usuário paga um pedido e a listagem não mostra o pedido; o suporte consulta o cliente e vê um status que o cliente já superou. Cada um desses sintomas tem padrão de mitigação conhecido, e o primeiro passo é tratá-los como decisões de design, e não como bugs aleatórios.

A composição da janela orienta onde atuar quando ela cresce. Publicação do evento responde por latência de infraestrutura; fila responde por espera e backlog; processamento responde por peso da projeção; gravação da visão responde por contenção no banco de leitura. Medir cada etapa separadamente transforma o número total de lag em diagnóstico acionável, e evita o erro comum de escalar tudo quando o problema está em uma etapa específica.

Existe ainda uma defasagem interna do lado de leitura que costuma ser esquecida. Se o read model usa réplicas de leitura para escalar consultas, a réplica pode estar atrás da primária, e a visão recém-atualizada não é vista por parte dos clientes, mesmo com a projeção em dia. Esse segundo nível de defasagem se soma ao primeiro e confunde diagnóstico, porque o painel de lag da projeção mostra zero enquanto o usuário reclama de dado velho. O tratamento é medir a defasagem da réplica separadamente e decidir por interação quais consultas podem tolerá-la.

O que a defasagem quebra em produção

Os efeitos práticos se concentram em três tipos de interação. A leitura imediata pós-escrita, quando o usuário espera ver o próprio efeito e não o vê, é a mais comum e a mais resolúvel na interface. A leitura cruzada entre usuários, quando um operador consulta dados que outro acabou de mudar, exige comunicação de frescor e, às vezes, indicadores visuais de atualização. As decisões automatizadas, quando um processo lê o read model para decidir, exigem cuidado adicional, porque automação sobre dado defasado pode executar lógica errada com confiança alta.

O caso clássico de automação sensibilizada: um robô de repricing lê a visão de estoque e ajusta preço; se a visão está defasada por dois minutos durante um pico de vendas, o robô decide sobre estoque inexistente. A resposta madura não é esconder a defasagem, é qualificar a leitura: a automação que exige frescor absoluto consulta o lado de escrita ou uma fonte com garantia mais forte, e a leitura exibitiva ao humano permanece no read model, onde a escala mora.

Quando a consistência imediata é exigida

Nem toda leitura pode aceitar defasagem, e mapear essas exceções é tarefa de desenho, não de improviso. O critério de negócio é simples: quando a consequência de decidir sobre dado velho é inaceitável, como risco financeiro, duplicidade de cobrança ou limite de crédito, a leitura pede garantia mais forte, e o custo extra é aceito conscientemente.

  • Verificação de limite antes de aprovar compra: consulta o lado de comando ou o agregado, e não a visão, porque saldo defasado vira prejuízo.
  • Checagem de duplicidade de pagamento: o detector consulta o modelo de escrita na janela crítica, onde a visão pode ainda não ter recebido o evento.
  • Fluxo de autorização com segundo aprovador: a leitura que alimenta a decisão usa versão esperada, garantindo que o aprovador vê o mesmo estado da requisição.
  • Operações de suporte sobre conta em disputa: leitura imediata com justificativa registrada, e não visão eventual que pode refletir estado superado.

O padrão de implementação para esses casos é a leitura qualificada: a API de leitura recebe um parâmetro ou um contexto que indica exigência de frescor, e o roteador direciona para o lado certo. Assim, a escala do read model continua servindo a maioria das consultas, e as poucas leituras críticas pagam o preço da garantia, com o custo visível em métrica e não escondido na média.

Read-your-own-writes na prática

O padrão de read-your-own-writes garante que cada usuário veja, no mínimo, os próprios efeitos. A mecânica usual aproveita a versão do stream: o comando devolve a versão que atingiu, o cliente guarda essa versão e a leitura espera, com teto curto, até que a visão alcance essa versão. Se a visão não alcança no teto, a resposta segue com o estado atual e um indicador de pendência, e nunca trava indefinidamente.

// Escrita devolve a versão do stream atingida pelo comando
public RespostaEscrita adicionarItem(String pedidoId, ItemDto item) {
    long versao = handler.executar(new AdicionarItemAoPedido(pedidoId, item.sku(), item.qtd()));
    return new RespostaEscrita(versao);   // cliente guarda essa versão
}

// Leitura com espera limitada pela versão escrita pelo próprio usuário
public ResumoPedidoDto obterPedido(String pedidoId, long versaoCliente) {
    ResumoPedidoDto dto = resumoDao.buscar(pedidoId);
    Instant limite = Instant.now().plusMillis(500);   // teto de espera curto

    while (dto.versaoProjecao() < versaoCliente && Instant.now().isBefore(limite)) {
        pausar(20);   // espera curta e limitada; melhor defasado do que travado
        dto = resumoDao.buscar(pedidoId);
    }
    return dto;   // se a visão atrasou, devolve o que tem, com flag de pendência
}

Duas propriedades tornam o padrão robusto. O teto de espera preserva o orçamento de latência da leitura, convertendo o pior caso em degradação informada, e não em timeout. E a versão da projeção no DTO permite ao cliente decidir com dado, exibindo pendência quando a visão está atrás da própria escrita. Alternativa comum quando a espera não é viável: responder a leitura direto do lado de escrita para o usuário autor da mudança, durante poucos segundos, e usar a visão para todos os outros.

Uma variação útil para aplicações com muitas requisições por sessão é o token de continuidade. A resposta de cada leitura devolve a versão da projeção que serviu o dado, e o cliente passa a enviar esse token nas leituras seguintes; o servidor só responde com visões de versão igual ou superior, esperando brevemente quando necessário. A garantia resultante é a de leituras monotônicas para o usuário: ele nunca vê o dado recuar no meio da navegação, mesmo que as requisições caiam em réplicas diferentes.

Sticky reads por sessão complementam o token: rotear temporariamente as leituras do usuário que acabou de escrever para a réplica mais fresca, ou para o próprio lado de escrita, durante uma janela curta, elimina a maior parte das expectativas quebradas sem esperar nada. A escolha entre esperar pela versão e rotear para fonte mais fresca é de custo: espera consome recurso de leitura, roteamento consome carga da fonte forte, e os dois caminhos precisam de teto temporal.

Versão esperada como contrato entre os lados

A versão do stream e a versão da projeção formam o par de números que sustenta as garantias do sistema inteiro. A versão do stream pertence ao lado de escrita: é a referência de concorrência otimista com que o comando declara sobre que estado decidiu, e um comando com versão esperada só é aceito se o stream estiver exatamente nela. A versão da projeção pertence ao lado de leitura: é o checkpoint da visão, gravado junto do read model, e indica até que ponto os eventos já foram materializados. Comparar esses dois números é o mecanismo que transforma a defasagem em decisão explícita, e não em surpresa.

O contrato de leitura com versão mínima formaliza essa comparação: o cliente envia, junto da consulta, a versão que sua escrita atingiu, e a API responde com dado fresco quando a projeção já alcançou a versão, ou com pendência explícita quando ainda não alcançou. A resposta pendente carrega o estado atual da visão e um sinal de frescor, permitindo que o cliente ou a interface decida entre esperar, recarregar depois ou seguir com o dado defasado assumido.

// Leitura por versão mínima: dado fresco ou pendência explícita, nunca mentira
public RespostaLeitura obterResumo(String pedidoId, long versaoEscrita) {
    ResumoPedidoDto dto = resumoDao.buscar(pedidoId);

    if (dto.versaoProjecao() >= versaoEscrita) {
        return RespostaLeitura.fresco(dto);            // visão alcançou a escrita
    }
    dto = esperarAteVersao(pedidoId, versaoEscrita, Duration.ofMillis(300));
    return dto.versaoProjecao() >= versaoEscrita
        ? RespostaLeitura.fresco(dto)
        : RespostaLeitura.pendente(dto);               // defasagem assumida no contrato
}

A mesma disciplina serve à decisão no lado de comando: um cancelamento enviado com a versão esperada garante que a recusa ou a aceitação considera exatamente o estado que o usuário viu na tela, e não um estado posterior criado por outra operação. Versão esperada no comando e versão mínima na leitura são as duas metades do mesmo princípio: nenhuma decisão ou exibição deve se apoiar em um estado que o autor não conhece. Times que adotam o par nas duas direções eliminam a classe inteira de bugs de estado invisível.

Padrões de experiência para o usuário

A interface é o lugar onde a defasagem se resolve na percepção. Padrões consolidados de experiência cobrem a maioria das situações.

  • UI otimista: aplicar o efeito na tela imediatamente, marcando como pendente, e confirmar ou reverter quando a leitura atualizar; o usuário percebe velocidade, e a inconsistência curta fica invisível.
  • Tela de confirmação com eco: após o comando, exibir os dados que o próprio usuário acabou de informar, e não a leitura do banco; a próxima visita usa a leitura eventual sem expectativa quebrada.
  • Estados de pendência visíveis: badges como processando, atualizando ou sincronizando transformam defasagem em status legível, e não em dado errado.
  • Notificação ou atualização automática: quando a visão alcança a versão, a tela recarrega a área afetada; o usuário vê o dado firmar sem recarregar manualmente.
  • Histórico de operações na tela: exibir o registro do próprio comando, com horário e estado pendente, e não apenas o resultado da leitura eventual.
  • Indicador de frescor nos dados exibidos: mostrar a idade da informação, como atualizado há 3 segundos, em painéis e listagens compartilhadas, convertendo a defasagem em contexto legível.
  • Reversão amigável na falha: quando a operação falha depois da UI otimista, desfazer com mensagem clara e estado consistente, e nunca deixar a tela pendente sem resolução.
  • Mensagens de suporte treinadas: atendimento com acesso ao log consulta o estado real e comunica defasagem com precisão, em vez de discutir com o cliente sobre quem está certo.

O fio condutor é não deixar o usuário descobrir a defasagem sozinho. Quando o sistema comunica o estado de sincronização, a defasagem vira parte do contrato percebido; quando o esconde, cada ocorrência parece um erro de confiança.

Defasagem e transações: os limites do padrão

A consistência eventual convive mal com expectativa transacional herdada do modelo único, e algumas fronteiras merecem atenção explícita. Uma transação de banco nunca cobre o trecho entre o lado de escrita e o read model quando estão em bases distintas; a garantia ali é de entrega e processamento, não de atomicidade. Da mesma forma, uma leitura do read model não serve como fonte para decisão dentro do comando, porque o comando decide sobre o agregado, e nunca sobre a visão eventual.

O desenho que respeita esses limites separa com clareza as garantias: o lado de escrita é transacional e forte; a sincronização é at-least-once com idempotência; a leitura é eventual com defasagem medida. Cada propriedade vive no componente certo, e nenhuma promessa cruza a fronteira sem conversão explícita. O erro clássico é prometer atomicidade ponta a ponta em documentação de API e depois explicar em incidente que a promessa era sobre a escrita apenas; o contrato precisa falar a língua do padrão desde o primeiro dia.

Medindo e limitando a defasagem

Defasagem não gerenciada se transforma em incidente; defasagem medida é capacidade operacional. As métricas mínimas são o lag por projeção, calculado como diferença entre a posição global do log e o checkpoint do consumidor, e a latência ponta a ponta, do comando confirmado até a visão atualizada, medida por eventos de amostra com timestamp. Painéis separam leitura exibitiva de automação, porque os orçamentos são diferentes.

Os limites entram como alertas com ação conhecida. Lag acima de um segundo por minuto contínuo abre alerta de degradação; lag acima de dez segundos abre incidente, com runbook que verifica consumidor, fila e store. Além da observação, o controle ativo conta com priorização de eventos, drenagem controlada de reprocessamentos e proteção do consumidor contra tempestades de reentrega. Em operação madura, o gráfico de lag é tão consultado quanto o de latência, porque ele é o pulso real do CQRS.

O rastreamento ponta a ponta fecha o ciclo de observabilidade. Cada comando carrega correlationId, os eventos herdam o identificador, e a projeção registra no log estruturado a posição processada; com isso, a pergunta por que a tela não atualizou vira uma consulta pela trilha, respondível com precisão de etapa. Times que instrumentam essa trilha desde o primeiro dia resolvem a maioria das reclamações de dado velho em minutos, e sem ela o sintoma vira mistério de múltiplos componentes.

Estratégia O que garante Custo Cenário típico
UI otimista com pendência Percepção de resposta imediata Implementação de estado na interface Telas de alta interação
Read-your-own-writes com versão Usuário vê o próprio efeito Espera limitada e versão no DTO APIs com leitura imediata pós-escrita
Leitura no lado de escrita por janela curta Consistência imediata para o autor Carga extra na escrita Pós-operação crítica, como pagamento
Automação lendo o lado de comando Decisão sobre dado fresco Perda de escala para o processo Robôs e integrações sensíveis
Sintoma em produção Causa provável Mitigação
Cliente reporta pagamento desaparecido na listagem Defasagem sem tratamento de percepção UI otimista e tela de confirmação com eco dos dados
Automação decide sobre estoque inexistente Processo crítico lendo visão eventual Roteamento de leitura qualificada para o lado de comando
Lag de projeção cresce em picos sem alerta Ausência de métrica e limites Monitorar lag por projeção com alertas e runbook
Suporte abre chamada de dado errado repetidamente Área não treinada sobre defasagem Painel de frescor e roteiro de atendimento com log

Consistência forte ou eventual: quem decide por interação

A escolha da garantia é por interação, e não por sistema inteiro. A tabela abaixo organiza a decisão pelos tipos de interação mais comuns em uma aplicação comercial.

Interação Garantia indicada Justificativa
Listagens e catálogos Eventual Escala domina; defasagem de segundos é invisível
Detalhe logo após o próprio comando Eventual com read-your-own-writes Autor precisa ver o próprio efeito
Aprovação financeira e limite Forte, no lado de comando Dado velho custa dinheiro
Painel analítico Eventual, com lag exibido Frescor de minutos é aceitável
Ação do suporte em disputa Forte com justificativa registrada Decisão sensível sobre estado real
Automação de preço e estoque Forte ou leitura qualificada Erro composto se multiplica

Erros comuns e boas práticas

  • Tratar a defasagem como bug e tentar sincronizar tudo em tempo real, sacrificando a escala que justificava a separação.
  • Deixar o teto de espera alto demais na leitura; espera de segundos degrada a experiência tanto quanto dado velho, e o teto deve caber no orçamento de latência.
  • Expor flag de pendência apenas para o frontend e escondê-la das automações; consumidores automáticos precisam do mesmo sinal para decidir.
  • Reprocessar a visão em produção sem comunicar defasagem às áreas de suporte e negócio; o número de aberturas de chamada falso multiplica.
  • Assumir que a fila garante ordem entre eventos de agregados diferentes; somente a ordem por stream existe, e visões que dependem de ordem global precisam de cuidado explícito.
  • Usar a leitura do read model como entrada de decisão dentro do comando; o comando decide sobre o agregado, e a visão eventual não é fonte de verdade.

Práticas que sustentam a operação: publique o lag de cada projeção no painel do produto, porque time de negócio também precisa saber o frescor dos dados; teste a leitura com defasagem simulada, verificando se a pendência aparece corretamente; documente por consulta qual garantia ela oferece, imediata ou eventual; e revise os tetos de espera a cada revisão de capacidade, alinhando-os com os orçamentos reais de latência. A consistência eventual deixa de ser risco quando passa a ser contrato medido, comunicado e limitado em todas as bordas do sistema.