O problema raramente é a mudança em si.

O problema é quando ela chega no tópico como se todo consumidor fosse atualizar junto.

Em sistemas distribuídos, isso quase nunca acontece.

Um produtor evolui hoje. Um consumer atualiza amanhã. Outro só vai ser corrigido na próxima sprint. Um quarto talvez nem esteja no radar de quem publicou a mudança.

É por isso que evolução de schema não pode depender de sincronia entre serviços.

No post sobre schema em eventos, a ideia central era que schema formaliza o contrato de dados. O passo seguinte é este: se existe contrato, ele precisa conseguir evoluir sem transformar cada alteração de payload em risco de quebra.

Em Kafka, evoluir schema com segurança significa permitir que produtor e consumidores convivam com versões diferentes do contrato por algum tempo.


O que realmente quer dizer evoluir schema

Muita gente trata evolução de schema como sinônimo de "mudar o payload".

Mas o ponto mais importante não é a mudança.

É a compatibilidade da mudança.

Se o produtor publica uma nova versão de um evento e os consumidores antigos continuam entendendo o que precisam entender, houve evolução segura.

Se a alteração exige que todo mundo atualize junto para o sistema continuar íntegro, isso não é evolução segura. É deploy coordenado disfarçado de arquitetura orientada a eventos.

Esse problema fica ainda pior quando o payload já nasceu informal, como vimos no post sobre JSON em eventos. Sem regra clara de compatibilidade, cada mudança parece pequena localmente, mas o efeito distribuído aparece depois.


O que normalmente quebra consumidores

Alguns tipos de mudança costumam ser especialmente perigosos:

  • remover um campo que consumidores antigos ainda usam
  • renomear atributos sem manter compatibilidade
  • mudar o tipo de um campo existente
  • alterar o significado de um valor mantendo o mesmo nome
  • transformar algo opcional em obrigatório sem transição

O ponto mais traiçoeiro é o último.

Mas existe outro caso que engana muita gente: enum.

Às vezes o time pensa que adicionar um novo valor é uma mudança segura porque o campo continua no mesmo lugar e com o mesmo nome.

Na prática, isso pode quebrar antes mesmo do runtime.

Quando aquele conjunto de valores é tratado como fechado e o modelo não prevê fallback para símbolos novos, a mudança já nasce incompatível. Em alguns cenários, a quebra aparece na validação de compatibilidade do contrato. Em outros, só quando um consumidor antigo recebe um valor que não sabe interpretar.

Nem toda quebra acontece porque o parser falhou.

Muitas vezes o consumer continua de pé, mas passa a interpretar o evento com uma regra antiga, ignora informação relevante ou grava dado inconsistente.

É a mesma lógica de falha tardia que apareceu no post sobre perda lógica no consumer: o sistema parece funcionando, mas o contrato já foi quebrado no significado.


O caminho mais seguro: mudanças aditivas e transição gradual

Na prática, a regra mais saudável costuma ser simples: primeiro adicione, depois migre, só então remova.

Se um campo novo precisa existir, o caminho mais seguro costuma ser adicioná-lo como opcional e permitir convivência com a estrutura antiga por um tempo.

Se um nome antigo precisa ser substituído, muitas vezes vale manter os dois durante a transição, com consumidores sendo adaptados gradualmente.

Se a mudança for incompatível de verdade, o mais prudente é tratar isso como nova versão de contrato, e não como uma simples alteração "inocente" do payload atual.

Esse raciocínio se conecta diretamente com o post sobre evento como contrato: contrato de integração não pode evoluir com a mesma liberdade de uma classe interna.

Ele precisa proteger quem está do outro lado.


Como pensar a sequência de rollout

Uma forma prática de pensar evolução segura é esta:

  1. o produtor passa a publicar de forma compatível com consumidores antigos
  2. os consumidores são migrados para entender a nova estrutura
  3. só depois o contrato antigo começa a ser removido

Essa ordem importa porque ela reduz a janela de quebra.

Quando o produtor muda primeiro de forma destrutiva, ele empurra risco para todo o ecossistema.

Quando a mudança já nasce compatível, os consumidores podem evoluir no próprio tempo sem que o sistema dependa de um deploy perfeitamente coordenado.

É isso que separa uma arquitetura realmente desacoplada de um conjunto de serviços que apenas trocam mensagens, mas continuam acoplados pelo calendário de release.


Boas práticas para evoluir sem improviso

Algumas decisões ajudam bastante:

  • tratar compatibilidade como parte do design do evento, não como correção posterior
  • preferir mudanças aditivas antes de mudanças destrutivas
  • manter semântica estável, e não apenas nome de campo estável
  • usar valores default com critério quando eles ajudarem versões diferentes do contrato a conviver
  • planejar transições com convivência temporária entre formatos quando necessário
  • assumir que consumidores diferentes vão atualizar em ritmos diferentes

Valor default costuma ser especialmente útil porque reduz a chance de que uma evolução simples vire incompatibilidade estrutural.

Mas ele só ajuda de verdade quando também faz sentido no domínio.

Default que existe apenas para "passar na compatibilidade" pode preservar o schema e ainda assim quebrar o significado do processamento.

Isso é importante porque, em produção, o evento não pertence apenas ao serviço produtor.

Depois que ele circula entre vários consumidores, pipelines e integrações, qualquer mudança incompatível deixa de ser detalhe local e vira risco distribuído.


O ponto que vale fixar

Evoluir schema com segurança não é evitar mudança.

É mudar sem exigir que todo consumidor descubra a alteração ao mesmo tempo.

Se o contrato de dados só continua válido quando todo mundo sobe junto, o sistema ainda está mais acoplado do que parece.

Em Kafka, evolução saudável de schema não é sobre velocidade de mudança.

É sobre compatibilidade suficiente para que a mudança aconteça sem quebrar quem ainda está no caminho.