Idempotência, consistência eventual e saga: dados corretos em sistemas distribuídos
Em um único banco de dados, manter os dados corretos é um problema resolvido: a transação garante que um conjunto de operações acontece por inteiro ou não acontece. Você debita de uma conta e credita em outra dentro de uma transação, e o banco assegura que as duas coisas ocorrem juntas ou nenhuma ocorre. Essa garantia é tão sólida que a gente esquece o luxo que ela representa. O choque vem quando o sistema se distribui: agora o débito está em um serviço e o crédito em outro, cada um com seu próprio banco, e não existe mais uma transação que abrace os dois.
Este guia trata do problema mais difícil dos sistemas distribuídos: manter os dados corretos quando a transação global desaparece. Enquanto padrões de resiliência cuidam de sobreviver a falhas de chamadas síncronas e a mensageria cuida do transporte, aqui o assunto é consistência de dados: como não perder nem duplicar informação quando várias partes precisam concordar sobre o que aconteceu.
Por que a transação não escala para o distribuído
A tentação inicial é recriar a transação única entre serviços, com protocolos como o two-phase commit, que tenta coordenar um commit atômico entre vários bancos. Na prática, ele escala mal e é frágil: trava recursos enquanto todos os participantes decidem, e a queda de um coordenador pode deixar o sistema em um estado indefinido. A indústria, na maioria dos casos, abandonou esse caminho para operações distribuídas de alto volume.
Em vez de forçar consistência imediata entre serviços, os sistemas distribuídos modernos adotam a consistência eventual, e desenham os dados para lidar com repetição e com estados intermediários. Isso muda a forma de pensar, e três conceitos sustentam essa mudança.
Idempotência: a base de tudo
Em sistemas distribuídos, mensagens se repetem. Uma resposta se perde na rede e o cliente reenvia; uma fila reentrega uma mensagem cujo processamento não foi confirmado. A entrega garantida quase sempre é at-least-once, ou seja, pelo menos uma vez, podendo ser mais. A consequência é inescapável: cada operação precisa ser idempotente, isto é, processá-la duas vezes tem que produzir o mesmo resultado que processá-la uma.
Uma operação como "definir o status para pago" é naturalmente idempotente: repetir não muda nada. Já "adicionar 100 ao saldo" não é: repetir credita 200. Para tornar segura uma operação que não é naturalmente idempotente, usa-se a chave de idempotência:
- O cliente gera um identificador único para a operação (por exemplo, um UUID) e o envia junto.
- O servidor registra as chaves já processadas.
- Se uma chave chega de novo, o servidor reconhece a repetição e devolve o resultado anterior, sem executar a operação outra vez.
A chave de idempotência é o mecanismo mais importante de corretude em sistemas distribuídos, e o primeiro a implementar em qualquer endpoint que altera dados.
O padrão outbox: não perca eventos
Um problema sutil derruba muitos sistemas: você grava um pedido no banco e precisa publicar um evento "pedido criado" numa fila. Se você grava no banco e o serviço cai antes de publicar o evento, o pedido existe mas ninguém foi avisado. Se publica o evento e a gravação no banco falha, avisou sobre um pedido que não existe. As duas ações não estão na mesma transação, e essa lacuna gera dados inconsistentes.
O padrão outbox resolve isso com elegância. Em vez de publicar o evento diretamente, você grava o evento em uma tabela outbox dentro da mesma transação que grava o pedido. Como as duas escritas estão na mesma transação do mesmo banco, ou as duas acontecem ou nenhuma acontece. Depois, um processo separado lê a tabela outbox e publica os eventos na fila, marcando cada um como enviado.
O resultado: o evento nunca se perde e nunca é publicado sem o dado correspondente existir. O outbox transforma "gravar e publicar" (duas ações que podiam divergir) em uma única escrita transacional, seguida de uma publicação confiável e reprocessável.
Saga: transações que se estendem por serviços
Quando um processo de negócio atravessa vários serviços (reservar estoque, cobrar pagamento, agendar entrega), não há transação única que garanta tudo ou nada. O padrão saga resolve isso quebrando o processo em uma sequência de passos locais, cada um transacional no seu próprio serviço, com uma regra crucial: para cada passo existe uma ação compensatória que o desfaz.
Se um passo falha no meio do caminho, a saga não faz rollback como um banco faria. Em vez disso, ela executa as compensações dos passos já concluídos, na ordem inversa, desfazendo o efeito de cada um. Cobrou o pagamento mas a entrega não pôde ser agendada? A saga dispara o estorno do pagamento e a liberação do estoque reservado.
Um exemplo de fluxo e suas compensações:
| Passo | Ação | Compensação se algo falhar depois |
|---|---|---|
| 1 | Reservar estoque | Liberar o estoque reservado |
| 2 | Cobrar pagamento | Estornar o pagamento |
| 3 | Agendar entrega | Cancelar o agendamento |
A saga aceita que o sistema passa por estados intermediários inconsistentes (por um instante, o pagamento foi cobrado mas a entrega ainda não existe) e garante que, no fim, ele chega a um estado coerente: ou tudo concluído, ou tudo compensado. As compensações precisam ser idempotentes, porque também podem ser reexecutadas em caso de falha.
Consistência eventual: pensar diferente
Todos esses padrões vivem sob o mesmo guarda-chuva: a consistência eventual. A ideia é que, após uma mudança, o sistema não fica coerente instantaneamente em todos os lugares, mas converge para um estado coerente em pouco tempo. Um dado atualizado em um serviço leva alguns instantes para se refletir em outro.
Isso exige desenhar a experiência do usuário para essa realidade. Nem todo dado tolera atraso, e a arte está em separar:
- O que exige consistência forte, imediata: um saldo financeiro antes de autorizar um saque, por exemplo. Isso deve viver dentro de uma única transação, em um único serviço.
- O que tolera consistência eventual: um contador de curtidas, um status que propaga em segundos, uma métrica agregada. A grande maioria dos dados de um sistema cabe aqui.
Forçar consistência forte onde a eventual bastaria custa escala e disponibilidade. Aceitar consistência eventual onde se exigia forte gera bugs de corretude. Classificar cada dado corretamente é uma decisão de arquitetura, não um detalhe.
E o exactly-once?
Muita gente sonha com a entrega exactly-once, a garantia de que cada mensagem é processada exatamente uma vez, nem mais nem menos. A verdade desconfortável é que exactly-once puro, no transporte, é praticamente impossível em um sistema distribuído sujeito a falhas de rede. O que existe na prática é uma combinação: entrega at-least-once no transporte, mais processamento idempotente no consumidor. O efeito final para o negócio é como se fosse exactly-once, porque, mesmo que a mensagem chegue duas vezes, a idempotência garante que ela só produz efeito uma vez. Perseguir exactly-once mágico é caçar um fantasma; construir consumidores idempotentes é a solução real e alcançável.
Corretude é decisão de projeto
Dados corretos em sistemas distribuídos não vêm de graça e não vêm depois. Eles vêm de decisões deliberadas tomadas no desenho: quais operações são idempotentes, quais eventos passam pelo outbox, quais processos viram sagas com compensação, e quais dados exigem consistência forte de verdade. A transação única do banco escondia toda essa complexidade de nós. Ao distribuir o sistema, herdamos a responsabilidade de garantir a corretude com as próprias mãos, e é o cuidado com esses padrões que separa um sistema distribuído confiável de um que, silenciosamente, perde e duplica dados até alguém perceber tarde demais.
Seu sistema distribuído perde ou duplica dados de vez em quando? Na ConstructorTI, projetamos consistência de dados com idempotência, padrão outbox e sagas com compensação, separando o que exige consistência forte do que tolera a eventual. Se pagamentos duplicam, eventos somem ou estados ficam incoerentes entre serviços, fale com a nossa engenharia e receba um diagnóstico da corretude dos seus dados distribuídos.