Exactly-once é uma daquelas expressões que parecem resolver tudo no slide.

Ler uma vez. Processar uma vez. Gerar efeito uma vez.

Bonito no discurso. Bem mais estreito na prática.

No Kafka, exactly-once não é uma garantia mágica para qualquer arquitetura. Ele resolve um problema específico, dentro de um escopo específico, com custo real de operação e desempenho.

Se você usar essa ferramenta no lugar certo, ela evita duplicidade onde ela realmente dói.

Se usar no lugar errado, você só ganha complexidade e continua precisando de idempotência do mesmo jeito.


A confusão mais comum

Muita gente escuta exactly-once e traduz assim:

"meu sistema inteiro agora processa cada evento exatamente uma vez".

Não é isso.

No Kafka, a semântica de exactly-once vale principalmente para fluxos em que consumo, processamento e nova publicação acontecem dentro do próprio ecossistema Kafka, coordenados por transação.

Em outras palavras:

  • o consumer lê do Kafka
  • processa o evento
  • publica resultado em outro tópico
  • confirma offsets dentro da mesma transação

Se tudo der certo, os dados publicados e o avanço do consumo aparecem juntos. Se der errado, nenhum dos dois aparece para leitores configurados corretamente.

Isso é valioso.

Mas repare no limite:

essa garantia não cobre automaticamente banco, API externa, e-mail, gateway de pagamento ou qualquer efeito colateral fora do log do Kafka.


O que o Kafka resolve de verdade com exactly-once

Quando exactly-once está bem configurado, o objetivo é evitar que um fluxo consume-transform-produce publique duplicado por causa de retry, reinício ou falha entre processamento e commit.

Na prática, isso combina algumas peças:

  • producer idempotente para não duplicar publicação em tentativas do próprio producer
  • transações para agrupar escrita em tópicos e commit de offsets
  • consumidores com read_committed para não enxergar resultado parcial ou abortado

O efeito prático é forte:

  • ou a transformação inteira fica visível
  • ou ela não fica visível para o restante da topologia

Esse modelo é especialmente útil quando o tópico de saída alimenta outros serviços, joins, agregações ou etapas posteriores que não deveriam receber duplicidade artificial.


Não confunda producer idempotente com exactly-once

Esse ponto merece separação clara.

enable.idempotence=true no producer costuma valer muito a pena.

Ele reduz o risco de duplicidade causada por retentativas do próprio producer ao publicar no mesmo tópico e partição. Em muitos cenários, isso deveria ser o padrão.

Mas isso ainda não é a mesma coisa que exactly-once.

Exactly-once com transações vai além:

  • coordena publicação de saída
  • coordena commit de offsets
  • define o que consumidores downstream podem enxergar

Ou seja:

  • producer idempotente resolve um pedaço do problema
  • exactly-once transacional resolve um fluxo maior

Misturar os dois conceitos costuma gerar arquitetura mal explicada e expectativa errada com o time.


Quando vale muito a pena

Exactly-once costuma fazer sentido quando a principal saída do processamento continua sendo Kafka.

Exemplos típicos:

  • uma aplicação lê de um tópico bruto, enriquece os eventos e publica em um tópico canônico
  • um pipeline faz deduplicação, normalização ou agregação e escreve o resultado em novos tópicos
  • uma topologia do Kafka Streams materializa estado e republica derivados
  • vários serviços dependem do tópico de saída e a duplicidade ali teria efeito em cascata

Nesses casos, o ganho é concreto porque o problema está exatamente no ponto que a transação do Kafka sabe proteger.

Se a pergunta é "quero evitar publicar duas vezes o resultado desta transformação Kafka para Kafka", a ferramenta é boa.

Se a pergunta é "quero garantir que meu banco e meu tópico evoluam como se fossem uma única transação distribuída", a resposta já é outra.


Quando normalmente não vale

Tem muito cenário em que exactly-once é tecnicamente possível, mas economicamente ruim.

Alguns exemplos:

  • consumidor simples que grava estado idempotente em banco com upsert
  • fluxo de telemetria, log ou métrica em que duplicidade pequena é aceitável
  • processamento em que o custo operacional da transação é maior do que o impacto da repetição
  • integrações em que o verdadeiro problema está no efeito externo, não na publicação em outro tópico

Também não costuma valer quando o time está tentando usar exactly-once como atalho para não desenhar idempotência.

Se o evento termina em banco, webhook, reserva, cobrança ou notificação, o gargalo real está na borda do negócio. Foi exatamente essa armadilha que apareceu no post sobre idempotência no consumer.


O ponto em que a promessa quebra

Imagine este fluxo:

  1. o consumer lê um evento do Kafka
  2. grava no banco
  3. chama uma API externa
  4. confirma o offset

Você pode até usar producer transacional em outra parte do fluxo.

Ainda assim, isso não transforma banco e HTTP em participantes nativos da transação do Kafka.

Se a aplicação:

  • gravar no banco
  • falhar antes de confirmar o offset
  • reiniciar e ler o evento de novo

então a duplicidade continua sendo um problema real.

O mesmo vale se a chamada externa aconteceu e o processo caiu antes do resto.

Por isso, exactly-once no Kafka não elimina a necessidade de:

  • idempotência
  • chave de deduplicação
  • upsert
  • outbox quando a publicação depende de estado persistido fora do Kafka
  • desenho cuidadoso de retry e DLT

Esse é o ponto que mais decepciona times que compraram o nome pela metade.

O marketing parece fim a fim. A garantia real é bem mais localizada.


O custo que vem junto

Não existe semântica mais forte sem custo.

Quando você liga transações e passa a depender de read_committed, entra um pacote de trade-offs:

  • mais coordenação interna
  • mais sensibilidade a configuração correta de transactional.id
  • throughput menor em alguns cenários
  • latência adicional
  • troubleshooting mais difícil
  • mais cuidado com fencing, abortos e observabilidade do fluxo

Nada disso significa que exactly-once é ruim.

Significa apenas que ele precisa pagar aluguel.

Se a duplicidade no tópico de saída é um risco caro, o aluguel compensa. Se o problema poderia ser resolvido com modelagem idempotente mais simples, talvez não.


O lugar onde ele brilha

O melhor terreno para exactly-once é este:

  • entrada no Kafka
  • processamento no Kafka
  • saída no Kafka

Nesse formato, o Kafka consegue proteger justamente a fronteira que importa.

Exemplo mental:

  • um tópico recebe eventos brutos de pagamento
  • uma aplicação valida e enriquece
  • publica em payments-validated
  • outro fluxo agrega e publica em payments-settled

Se uma falha acontecer no meio, o objetivo é impedir que um estágio intermediário publique saída duplicada ou parcialmente visível.

Aqui, exactly-once conversa bem com o problema.

Principalmente quando a duplicidade num tópico intermediário contaminaria o resto da topologia.


O lugar onde ele vira distração

Agora troque o cenário:

  • entrada no Kafka
  • processamento na aplicação
  • gravação em banco relacional
  • chamada para sistema externo
  • eventual publicação de auditoria

Aqui, discutir exactly-once no Kafka antes de discutir consistência de borda costuma ser distração arquitetural.

Os problemas mais caros não estão no broker:

  • estão na repetição de efeito externo
  • na falta de idempotência no consumer
  • no retry sem critério
  • na falsa sensação de segurança ao empurrar falha para DLT

Se o estado real do negócio mora fora do Kafka, a pergunta principal não é "a transação do Kafka está ligada?".

A pergunta principal é:

se eu processar esse evento de novo, o estado final continua correto?


Uma regra prática que costuma funcionar

Se o resultado principal do seu processamento é outro tópico Kafka, comece avaliando exactly-once.

Se o resultado principal é um efeito externo, comece avaliando idempotência, outbox, deduplicação e estratégia de retry.

Essa ordem é importante porque ela organiza a arquitetura em torno do risco real, não em torno do nome mais sedutor.

Uma heurística útil:

  • enable.idempotence=true quase sempre merece consideração séria no producer
  • exactly-once transacional merece consideração séria em pipelines Kafka para Kafka
  • idempotência no consumer continua obrigatória quando existe efeito externo
  • retry e commit precisam ser pensados junto da semântica de processamento, não depois

O ponto que vale fixar

Exactly-once no Kafka não é mentira.

Mas também não é milagre.

Ele é uma ferramenta excelente quando o seu problema é evitar duplicidade e inconsistência em fluxos transacionais dentro do próprio Kafka.

Fora desse escopo, ele não substitui arquitetura de borda, idempotência nem modelagem cuidadosa de efeito colateral.

Quando o problema é Kafka para Kafka, ele pode valer muito a pena.

Quando o problema é mundo real para fora do broker, muitas vezes o nome impressiona mais do que resolve.