No ecossistema de vendas que mantenho, aplicativos Android e Windows operam em versões diferentes, guardam dados em SQLite e precisam continuar trabalhando sem conexão. Quando a rede volta, sincronizam com serviços, PostgreSQL e ERP. Uma publicação no servidor não atualiza os dispositivos em conjunto. Alguns clientes podem ficar offline justamente durante a transição.

Esse contexto muda a pergunta sobre versionamento. O problema não é escolher entre v1, v2 ou uma versão SemVer mais elegante. É decidir quais clientes, formatos e estruturas de dados precisam coexistir, por quanto tempo e sob quais evidências o caminho antigo poderá ser removido. A janela nasce quando a primeira mudança exige coexistência. Ela termina quando a operação prova que o legado deixou de ser necessário.

01

A janela nasce antes do primeiro deploy

Uma alteração aparentemente aditiva já pode abrir a janela. O servidor passa a devolver um novo campo, uma tabela ganha uma coluna, ou um evento recebe outro atributo. O código novo entende os dois estados; o antigo, talvez não. A partir daí, versão de contrato, implementação e implantação deixam de coincidir.

No caso dos aplicativos de vendas, a implantação é desigual por natureza. Há duas plataformas, conectividade intermitente e bases locais. Por isso, uma política fixa como "suportar N e N-1" seria apenas uma aproximação. Em alguns rollouts internos, essas duas versões podem representar toda a coexistência necessária. Para clientes offline ou atualizados em ritmos diferentes, a janela precisa cobrir todas as versões que ainda podem sincronizar ou voltar após um período sem conexão.

Antes de mudar código, eu materializaria uma matriz pequena, ligada a um release concreto:

  • Cliente Android atual com API nova: suportado; caminho principal e observado.
  • Cliente Android anterior com API nova: suportado durante a janela; leitura e sincronização antigas ainda aceitas.
  • Cliente Windows ainda em distribuição com API nova: suportado durante a janela; responsável conhecido e adoção medida separadamente.
  • Cliente fora da política publicada: não suportado; bloqueio explícito ou atualização exigida, sem fallback silencioso.
  • Banco local antigo enviando dados ao schema central expandido: tolerado para migração; adaptação com data e critério de remoção.

A matriz não tenta catalogar o sistema inteiro. Ela registra uma promessa operacional: combinação, duração, evidência de uso e responsável pela retirada. Se uma célula não tem dono ou não pode ser observada, a equipe ainda não sabe como fechá-la.

SemVer ajuda pouco nessa decisão. Ele comunica intenção para uma API pública: MAJOR indica incompatibilidade; MINOR, funcionalidade compatível; PATCH, correção compatível.[1] É útil em bibliotecas e artefatos, mas não coordena dispositivos offline, schemas persistidos nem ordem de implantação. Uma mudança MINOR pode quebrar um desserializador rígido; um PATCH pode corrigir um comportamento do qual algum cliente dependia. O número identifica a mudança. A matriz define a obrigação.

02

Na API, compatibilidade depende do cliente que ficou para trás

Durante a janela, a API nova precisa atender clientes que não avançaram. Isso favorece mudanças aditivas, mas "aditiva" não significa "segura". Um campo desconhecido pode ser rejeitado, um enum novo pode quebrar um switch exaustivo e uma alteração de semântica pode preservar o JSON enquanto muda o resultado. O AIP-180 separa compatibilidade de código-fonte, de wire e semântica justamente porque bytes válidos não garantem comportamento válido.[2]

Um leitor tolerante reduz parte desse risco: o cliente lê o que usa e ignora extensões declaradas como opcionais. A tolerância precisa ter fronteira. Valores monetários, autenticação, limites e comandos irreversíveis pedem validação estrita. No HTTP/1.1, o RFC 9112 alerta que diferenças de parsing leniente entre destinatários podem contribuir para request smuggling; outros casos de parsing inválido também podem abrir espaço para response splitting.[5] Não é prudente aplicar o princípio de Postel a framing ou segurança como se toda permissividade fosse benigna.[4]

Quando a dependência de um cliente não aparece na especificação, contratos dirigidos pelo consumidor podem tornar o uso real verificável. O Pact, por exemplo, gera o contrato a partir dos testes automatizados do consumidor.[6] Eu usaria CDCT onde a criticidade e o número de integrações justificassem seu custo, validando apenas os campos e comportamentos usados. Bloquear uma promoção por falta de combinação verificada é uma política contextual: depende da confiabilidade do broker de contratos, de uma rota de emergência e do dano possível. CDCT não substitui teste funcional nem deve congelar a resposta inteira.

A versão pode ser explícita no caminho, em query string, header ou media type. O AIP-185 põe a major no caminho REST; a política de serviços do Azure usa api-version datada na query string.[3][12] Nenhum formato elimina a coexistência. Uma nova major só ajuda se houver roteamento previsível, substituto operável e prazo de migração.

Para APIs cliente-servidor, prefiro evitar os rótulos "backward" e "forward" isolados. Escrever "cliente v1 funciona com servidor v2" não deixa a direção em dúvida. Negociar capacidades também pode ser melhor quando uma função opcional varia por plataforma, região ou rollout: o cliente pergunta se partial-refund está disponível, em vez de inferir isso da versão. Essa escolha adiciona estados, cache, downgrade, fallback e casos de teste. Eu a reservaria para capacidades independentes, não como substituto geral do versionamento.

03

No banco, a janela atravessa código e dados

O servidor pode voltar para um binário anterior; os dados transformados não voltam automaticamente. Por isso, mudança de schema precisa admitir código antigo e novo durante rollout, rollback e processamento atrasado.

O padrão expand–migrate–contract aplica ao schema uma lógica relacionada a Parallel Change, mas os termos não são sinônimos universais.[7] Em uma alteração de dados, a sequência é concreta. Primeiro, expandir o schema sem retirar o formato antigo. Depois, migrar leitores, escritores e dados com backfill idempotente e reconciliação. Só então contrair a estrutura legada.

No ecossistema de vendas, a base central não é o único estado persistido. Dispositivos podem manter SQLite antigo e enviar dados depois. Isso prolonga a fase de expansão: o servidor precisa aceitar o payload anterior, adaptá-lo de modo determinístico e registrar qual caminho foi usado. Escrita dupla, quando necessária, exige fonte de verdade, regra de precedência e detecção de divergência. Mantê-la indefinidamente apenas troca uma migração por dois modelos permanentes.

Uma política conservadora deste artigo é separar a mudança destrutiva por pelo menos uma janela completa de implantação e rollback. Essa janela não é "um release" por definição. Ela deve cobrir o tempo máximo em que réplicas antigas, filas atrasadas ou clientes offline ainda podem reaparecer. Se a telemetria não distingue leitura e escrita no formato antigo, remover coluna, trigger ou adaptador continua sendo aposta.

Expandir preserva a opção de voltar; migrar produz evidência; contrair encerra a dívida transitória. Confundir as três fases faz a equipe tratar a chegada do código novo como fim da mudança, quando os dados antigos ainda mantêm a janela aberta.

04

Em eventos, a janela pode durar mais que o produtor

Eventos desacoplam implantação e também desacoplam tempo. Uma mensagem produzida hoje pode ser lida por um consumidor antigo, reprocessada por um consumidor novo ou reaparecer em um replay muito depois de o produtor mudar. A política de retenção, portanto, participa da matriz.

Para schemas e eventos, seguindo a convenção do Confluent Schema Registry, backward compatibility significa que consumidores com schema novo leem dados antigos. Forward compatibility significa que consumidores com schema antigo leem dados novos. Full reúne as duas direções. A variante transitive compara com todas as versões anteriores, enquanto a não transitiva pode verificar apenas a versão imediatamente anterior.[9] As regras concretas variam entre Avro, Protobuf e JSON Schema; o nome da política não substitui teste com o formato adotado.

Protocol Buffers mostra por que o formato importa. Na serialização binária, runtimes modernos preservam campos desconhecidos ao analisar e serializar novamente a mesma mensagem, desde que ela não seja reconstruída campo a campo. Essa garantia não se estende automaticamente a ProtoJSON: campos desconhecidos não são propagados da mesma forma e parsers podem rejeitá-los.[8] Dizer apenas "Protobuf é compatível" apaga a diferença que decide se um intermediário perde informação.

Eu recomendaria um envelope de evento com identidade, tipo, versão de schema, instante e correlação quando esses dados forem necessários para deduplicação, roteamento, auditoria ou diagnóstico. É uma recomendação de design, não uma exigência universal de registry. O schema também pode ser identificado fora do payload, e padrões como CloudEvents oferecem outra convenção.

A janela de um evento não fecha quando o último produtor antigo sai de produção. Fecha quando consumidores suportados, retenção e replay não dependem mais do formato anterior. Se N só foi verificado contra N-1, mas o replay alcança N-3, a compatibilidade declarada não cobre a operação real.

05

Observar é o que permite fechar

Deprecar não é marcar um endpoint como antigo. É iniciar uma migração com substituto, prazo, responsável e evidência. No ecossistema de vendas, eu separaria a telemetria por plataforma, versão de cliente, operação e caminho de compatibilidade. O objetivo não é colecionar versões em dashboard, mas responder: quem ainda usa o contrato antigo, quando usou pela última vez e se consegue operar o substituto?

Kubernetes dá um exemplo útil ao combinar política de depreciação com a métrica apiserver_requested_deprecated_apis, que identifica grupo, versão, recurso, subrecurso e release de remoção.[10][11] A lição não é copiar seus prazos. É ligar a retirada a sinais observáveis.

Um critério conservador pode exigir zero uso conhecido por um período que cubra retenção e offline, confirmação dos responsáveis conhecidos e análise do tráfego não identificado. Prazo vencido sem evidência pede investigação, não desligamento automático. Uso residual sem responsável também não justifica eternizar o legado; exige decisão explícita sobre bloqueio, exceção ou risco aceito.

Canary pode reduzir impacto quando a plataforma permite dividir tráfego de forma representativa e distinguir o grupo em logs e métricas, como no Amazon API Gateway quando configurado.[13] Ele não ajuda se o cliente offline não participa da amostra, se a alteração de banco é irreversível ou se o volume pequeno não expõe o problema. Nesses casos, sandbox, opt-in, validação por coorte ou rollout por cliente podem produzir evidência melhor.

Antes da retirada, a equipe precisa testar o que acontecerá sem o caminho antigo: bloquear em ambiente controlado, observar erros, conferir sincronizações pendentes e exercitar rollback do sistema completo, inclusive dados e efeitos externos. Segurança ou compliance podem encurtar a janela, mas não dispensam responsável, comunicação e mitigação.

O número da versão é só o índice. A compatibilidade existe no intervalo entre introduzir uma mudança e provar que o caminho anterior pode sair. Se esse intervalo não tem matriz, telemetria e critério de encerramento, a organização não administra uma janela. Apenas acumula versões.

06

Referências

  1. Semantic Versioning 2.0.0
  2. AIP-180: Backwards compatibility
  3. AIP-185: API Versioning
  4. RFC 1122: Requirements for Internet Hosts — Communication Layers
  5. RFC 9112: HTTP/1.1
  6. Pact Docs: Introduction
  7. Parallel Change
  8. Protocol Buffers: Language Guide (proto 3)
  9. Confluent Schema Evolution and Compatibility
  10. Kubernetes Deprecation Policy
  11. Kubernetes Metrics Reference
  12. Azure Versioning Policy for Services, SDKs, and CLI Tools
  13. Amazon API Gateway Canary Release Deployment