Idempotência costuma ser discutida tarde demais.
O evento já foi publicado, o consumer já recebeu a mensagem, algum efeito externo já aconteceu, e só então o time pergunta:
"como eu evito processar isso duas vezes?"
Essa pergunta é importante, mas chega incompleta.
Se o produtor publica eventos sem identidade estável, sem semântica clara e sem relação confiável com a mudança de estado que originou o fato, o consumer herda um problema que talvez não consiga resolver sozinho.
Consumer idempotente continua sendo obrigatório em muitos fluxos. Mas idempotência em Kafka não começa no @KafkaListener, no poll loop ou na tabela de eventos processados.
Ela começa antes: no desenho do evento, na forma como ele é produzido e na fronteira onde a mudança de negócio vira mensagem.
O erro de jogar tudo para o consumer
É comum tratar idempotência como uma preocupação exclusiva de quem consome.
O raciocínio parece simples:
- Kafka pode permitir reprocessamento;
- o consumer pode receber o mesmo registro novamente;
- então o consumer precisa deduplicar.
Isso está certo, mas não está completo.
O problema aparece quando o consumer precisa decidir se duas mensagens representam o mesmo fato e não tem dados suficientes para isso.
Imagine dois eventos quase iguais:
{
"orderId": "order-123",
"status": "PAID",
"createdAt": "2026-09-10T10:15:00Z"
}
Se esse evento aparecer duas vezes, ele é duplicado?
Talvez sim.
Mas também pode ser uma correção, uma republicação, uma tentativa de reconstrução, uma segunda autorização, uma nova versão do mesmo processo ou apenas um evento mal nomeado.
Sem uma identidade de operação, o consumer começa a deduzir semântica a partir de campos de negócio que talvez não tenham sido criados para isso.
E dedução não é garantia.
Idempotência precisa de identidade
Para um consumer tratar duplicidade com segurança, ele precisa de alguma forma confiável de responder:
"eu já apliquei este fato?"
Essa resposta normalmente depende de uma chave estável.
Pode ser:
eventId;operationId;commandId;- identificador de pagamento, reserva ou transferência;
- chave natural de uma operação de negócio;
- versão de um agregado, quando o modelo exige ordenação e evolução de estado.
O nome importa menos que a propriedade.
A chave precisa representar a identidade do fato ou da operação que não deve produzir efeito duplicado.
Se o produtor gera um identificador novo a cada retry, ele destrói a deduplicação.
Se o produtor usa apenas timestamp, ele transforma idempotência em aposta.
Se o produtor publica eventos diferentes com a mesma chave de idempotência, ele força o consumer a escolher entre ignorar algo legítimo ou repetir algo perigoso.
Por isso a chave de idempotência não é detalhe técnico local do consumer.
Ela é parte do contrato do evento.
Producer idempotente não resolve idempotência de negócio
Existe outra confusão frequente: achar que enable.idempotence=true fecha o assunto.
Essa configuração é útil.
Ela ajuda o producer Kafka a evitar duplicidade causada por retentativas de publicação para o mesmo tópico e partição, dentro das regras do próprio produtor.
Mas isso não significa que o seu fluxo de negócio ficou idempotente.
O producer idempotente atua na escrita no log.
Ele não sabe se PaymentApproved representa uma cobrança única, uma tentativa de cobrança, uma atualização de status ou uma compensação.
Ele também não cria sozinho uma identidade de negócio estável para o evento.
Compare os dois problemas:
Problema Kafka:
o producer tentou publicar, houve falha de rede, ele retentou e não deve duplicar o registro no log.
Problema de negócio:
o mesmo pagamento chegou duas vezes ao sistema e não pode gerar duas baixas, dois e-mails ou duas liquidações.
São problemas relacionados, mas não são o mesmo problema.
Resolver o primeiro não elimina a responsabilidade pelo segundo.
O contrato do evento também participa
Evento não deveria ser apenas um pacote de campos convenientes.
Ele precisa carregar informação suficiente para os consumidores tomarem decisões corretas sem conhecer detalhes internos do produtor.
Quando o assunto é idempotência, isso geralmente inclui responder algumas perguntas:
- que fato este evento representa?
- qual é a identidade única desse fato?
- repetir este evento significa duplicidade ou nova ocorrência?
- existe uma versão ou sequência relevante?
- qual campo deve ser usado para deduplicação?
- por quanto tempo essa deduplicação precisa ser lembrada?
Nem toda resposta precisa virar campo no payload.
Algumas pertencem à documentação do contrato, ao schema, à convenção de headers ou à modelagem do tópico.
Mas elas precisam existir.
Um consumer não deveria descobrir em produção que orderId não basta para deduplicar porque um mesmo pedido pode ter várias tentativas de pagamento.
Também não deveria descobrir tarde demais que paymentId muda a cada retry do produtor, mesmo quando a intenção de negócio é a mesma.
Esse tipo de ambiguidade transforma idempotência em remendo.
A publicação também precisa ser confiável
Existe ainda uma fronteira anterior ao Kafka que muita arquitetura ignora:
a mudança de estado que gera o evento.
Se o serviço grava no banco e depois publica no Kafka, existe uma janela de falha entre as duas operações.
Se ele publica no Kafka e depois grava no banco, existe outra janela.
Esse problema não é apenas de consistência.
Ele também afeta idempotência.
Um serviço pode tentar publicar de novo porque não sabe se a tentativa anterior chegou ao broker.
Pode reconstruir eventos a partir do banco.
Pode emitir duas mensagens para a mesma mudança de estado.
Pode publicar um evento sem a chave correta porque a operação original não foi persistida com uma identidade estável.
É por isso que o Outbox Pattern conversa diretamente com idempotência.
Ao persistir a mudança de negócio e a intenção de publicar o evento na mesma transação local, o sistema ganha um lugar mais confiável para guardar eventId, operationId, payload e estado de publicação.
Outbox não substitui consumer idempotente.
Mas evita que o consumer receba um problema fabricado por uma fronteira de publicação frágil.
O consumer continua responsável
Nada disso absolve o consumer.
Mesmo com producer bem configurado, evento bem modelado e outbox corretamente implementado, o consumer ainda precisa tolerar repetição.
Ele pode cair depois de gravar no banco e antes de confirmar offset.
Pode sofrer rebalance.
Pode ser reexecutado a partir de um offset antigo.
Pode receber replay planejado.
Pode processar mensagens antigas depois de uma correção operacional.
Foi esse o ponto do post sobre idempotência no consumer: se repetir um evento muda o resultado final indevidamente, o processamento está frágil.
A diferença é que o consumer não deveria carregar essa responsabilidade sozinho.
Ele deveria receber um evento que permite implementar idempotência de forma objetiva, persistente e auditável.
Sem isso, o código de consumo vira um conjunto de heurísticas:
- compara campos;
- consulta estados indiretos;
- tenta inferir intenção;
- ignora eventos "parecidos";
- aceita duplicidade porque não consegue provar nada.
Esse desenho não escala bem operacionalmente.
Um exemplo simples
Considere um fluxo de pagamento.
Um comando externo solicita a aprovação de uma cobrança.
O serviço de pagamentos confirma a operação no banco e publica um evento.
Um evento pobre para idempotência seria algo assim:
{
"orderId": "order-123",
"amount": 199.90,
"status": "APPROVED"
}
O consumer consegue atualizar uma projeção simples com isso.
Mas se o evento reaparecer, ou se houver duas tentativas de pagamento para o mesmo pedido, a semântica fica ruim.
Um contrato mais útil poderia separar melhor as identidades:
{
"eventId": "evt-789",
"operationId": "pay-456",
"orderId": "order-123",
"paymentId": "payment-456",
"amount": 199.90,
"status": "APPROVED",
"occurredAt": "2026-09-10T10:15:00Z"
}
Agora o consumer pode decidir com mais segurança.
Ele pode usar eventId para saber se já viu aquele evento específico.
Pode usar operationId ou paymentId para proteger o efeito de negócio.
Pode tratar orderId como referência do agregado, não como prova única de deduplicação.
O ponto não é copiar esse payload.
O ponto é desenhar identidade antes de precisar remendar duplicidade.
O ponto que vale fixar
Idempotência em Kafka não é uma responsabilidade isolada no fim da cadeia.
O consumer precisa ser idempotente, mas ele precisa receber condições para isso.
O produtor precisa publicar eventos com identidade estável.
O contrato precisa deixar claro o que representa uma ocorrência única.
A fronteira entre banco e Kafka precisa preservar a intenção de publicação sem criar duplicidade artificial.
E só então o consumer consegue fazer sua parte com segurança.
Quando idempotência fica apenas no consumer, ela vira reação.
Quando entra no desenho do evento e da publicação, ela vira parte da arquitetura.
