Mudanças que quebram algo: o que conta e como lançá-las
10 min de leitura atualizado em
Uma mudança que quebra algo é uma mudança que um chamador escrito corretamente não conseguiria sobreviver. A definição importa porque a maioria dos debates sobre se algo “conta” na verdade são debates sobre quem estava segurando errado. Se um chamador seguiu a documentação de vocês e a mudança de vocês fez o código dele parar de funcionar, a mudança quebrava algo. O que vocês pretendiam não tem nada a ver com isso.
Esse é todo o teste. O resto deste artigo é o que decorre disso: o que falha no teste, o que passa, como pegar uma falha antes que ela seja mergeada, e o que fazer assim que você sabe que está lançando uma.
O que conta como uma mudança que quebra algo?
Aplique o teste ao chamador, não ao diff. Uma mudança quebra algo quando um chamador que dependia apenas de comportamento documentado precisa mudar seu código, sua configuração ou seus dados para continuar funcionando. Remover um campo, renomear um endpoint, apertar a validação, mudar um padrão e mudar o tipo de um valor se qualificam todos. Adicionar um campo opcional não se qualifica. Corrigir um bug geralmente não se qualifica, com uma exceção importante abaixo.
| Mudança | Quebra algo? | Por quê |
|---|---|---|
| Remover ou renomear um campo, endpoint, flag ou opção | Sim | Chamadores corretos referenciam isso |
| Adicionar um campo opcional ou um novo endpoint | Não | Chamadas existentes não mudam |
| Tornar uma entrada opcional obrigatória | Sim | Chamadas que omitiam falham agora |
| Apertar validação previamente aceita | Sim | Entradas que funcionavam agora são rejeitadas |
| Mudar um valor padrão | Sim | Chamadores que não o definiram recebem comportamento novo |
| Mudar um tipo (string para número, valor único para array) | Sim | Parsers escritos para o tipo documentado falham |
| Reordenar as chaves de um objeto | Não | A menos que vocês tenham documentado a ordem |
| Corrigir um bug do qual chamadores dependiam | Na prática, sim | Ver a seção sobre contratos acidentais |
| Elevar um limite de taxa ou um teto de tamanho | Não | Nada que funcionava para de funcionar |
| Reduzir um limite de taxa ou um teto de tamanho | Sim | Tráfego que estava bem agora é limitado |
| Mudar a formulação de uma mensagem de erro | Depende | Quebra algo se vocês documentaram ou chamadores dão match nisso |
O que não é uma mudança que quebra algo?
Uma mudança não quebra nada quando toda chamada que funcionava antes continua funcionando, sem alteração, e continua significando a mesma coisa. Adicionar um novo endpoint, adicionar um parâmetro opcional de requisição, adicionar um campo a uma resposta, tornar uma entrada obrigatória opcional, elevar um limite e melhorar uma mensagem de erro em que ninguém dá match passam todos no teste. Essas mudanças aditivas podem sair em uma release minor com uma entrada de changelog comum.
Mudanças aditivas ainda quebram chamadores em três situações. Um cliente cujo deserializador rejeita campos desconhecidos falha no primeiro campo novo da resposta, então documente cedo que os chamadores devem ignorar campos que não reconhecem. Um novo valor de enum quebra todo chamador com um switch exaustivo (mais sobre isso abaixo). E uma resposta que cresce pode empurrar um chamador além de um limite de tamanho, de um timeout ou de uma largura de coluna em que ele nunca precisou pensar.
Quatro linhas da tabela merecem um olhar mais atento, porque é onde as discordâncias acontecem.
As quatro mudanças que quebram algo que as equipes ignoram
Contratos acidentais. Se a API de vocês retornou o mesmo campo não documentado por três anos, um chamador construiu em cima disso. A Lei de Hyrum é a versão curta: com usuários suficientes, todo comportamento observável do sistema de vocês terá alguém dependendo dele. É por isso que “foi uma correção de bug” não é uma defesa. A correção pode estar correta e ainda assim quebrar algo. Lancem-na como tal.
Mudanças de comportamento sem mudança de esquema. O campo ainda está lá, o tipo é o mesmo, e o
valor agora significa algo diferente. Um status que costumava ser active ou inactive e agora
também retorna suspended quebra todo chamador com um switch exaustivo. Um timestamp que muda de
hora local para UTC quebra todo mundo que não leu a documentação duas vezes. Nada em um diff do
arquivo OpenAPI mostra isso.
Validação apertada. Vocês começam a rejeitar e-mails sem TLD, ou espaços em branco no final, ou nomes com mais de 80 caracteres. Todo chamador que estava enviando exatamente isso agora recebe um 400 para uma requisição que funcionava na semana passada. Mudanças de validação são as mais comumente lançadas como uma correção de “endurecimento”.
Padrões alterados. Ninguém que definiu o valor explicitamente percebe nada. Todos que não definiram, que são a maioria dos chamadores, recebem comportamento novo sem mudar uma linha. Um padrão alterado quebra a maioria dos usuários de vocês exatamente porque eles nunca viram a configuração.
Como detectar uma mudança que quebra algo antes que ela seja lançada?
Compare o contrato do pull request com o contrato da branch principal, no CI, e faça o build falhar diante de uma diferença que quebra algo. Existem ferramentas de diff de esquema para a maioria dos formatos de interface, e cada uma conhece as regras de quebra do seu próprio formato:
| Interface | Ferramenta | O que compara |
|---|---|---|
| REST (OpenAPI) | oasdiff | Duas specs OpenAPI, com um relatório de mudanças que quebram algo |
| gRPC (Protobuf) | buf breaking | Arquivos .proto, no nível de wire ou de código-fonte |
| GraphQL | GraphQL Inspector | Dois esquemas, sinalizando mudanças que quebram algo e mudanças perigosas |
| Crates Rust | cargo-semver-checks | A API pública contra a última versão publicada |
| Pacotes TypeScript | API Extractor | Um relatório versionado da API pública do pacote |
Essas ferramentas pegam com confiabilidade campos removidos, operações renomeadas e tipos alterados. Elas não enxergam os dois primeiros dos quatro tipos acima, um contrato acidental ou uma mudança de comportamento, porque nenhum dos dois aparece em um esquema. Use a ferramenta para barrar as óbvias e a pergunta de revisão “um chamador correto poderia notar isso?” para o resto. O mesmo job de CI é um lugar natural para exigir uma entrada de changelog, como descrito em exigir entradas de changelog no CI, e mudanças de API em gRPC e Protobuf percorre os casos no nível de wire.
Como marcar uma mudança que quebra algo em um commit?
Com Conventional Commits, uma mudança que quebra
algo é marcada por um ! antes dos dois-pontos (feat(api)!: remove the legacy export endpoint) ou
por um rodapé que começa com BREAKING CHANGE: seguido de uma descrição. Qualquer um dos dois
corresponde a uma versão major. Escreva o rodapé como o primeiro rascunho da entrada de changelog,
dizendo quem é afetado e o que deve fazer.
Conventional commits e o changelog mostra até onde a
convenção leva vocês.
A mesma regra vale para bibliotecas. Uma função pública removida, um tipo de parâmetro estreitado ou um valor de retorno alterado é uma versão major sob versionamento semântico. As bibliotecas nem sempre a seguem: um estudo de 119.879 atualizações do Maven Central descobriu que 16,6% quebraram o versionamento semântico, mas apenas 7,9% dos projetos clientes foram afetados, porque a maioria dessas mudanças tocava código que nenhum cliente chamava. A quebra se mede no chamador.
Como se lança uma mudança que quebra algo?
Você a lança abertamente, com uma data, com um caminho. Os passos abaixo estão em ordem, e o último é o que a maioria das equipes pula: dizer às pessoas que foram afetadas que o que estavam esperando aconteceu agora.
- Decida se é uma. Use o teste acima, não o diff. Se dois engenheiros discordam, quebra algo; a discordância é evidência de que um chamador poderia razoavelmente ter dependido do comportamento antigo.
- Versione. Sob versionamento semântico uma mudança que quebra algo é uma versão major. Se vocês operam uma API datada ou versionada, ela vai para uma nova versão e a antiga continua funcionando até uma data declarada. Se vocês não podem versionar, não estão lançando uma mudança que quebra algo, estão lançando uma interrupção com uma entrada de changelog. Qual esquema carrega a versão é o assunto de melhores práticas de versionamento de API.
- Escreva a entrada antes que o código seja mergeado. A entrada tem uma forma fixa: o que muda, quem é afetado, o que devem fazer, e até quando. Se vocês não conseguem preencher os quatro, a mudança não está pronta. O template de release notes coloca essas entradas primeiro, com uma data em vez de um número de versão, exatamente por isso.
- Dê um prazo, não um número de release. “Removido na v5” não significa nada para quem não acompanha as releases de vocês. “Deixa de funcionar em 1º de novembro de 2026” significa a mesma coisa para todos.
- Forneça a migração. A chamada antiga ao lado da nova. Se a mudança é uma renomeação, diga os dois nomes na mesma frase; se é um campo removido, diga para onde os dados foram.
- Anuncie em todo lugar onde o comportamento antigo estava documentado. O changelog, a página de docs do endpoint, as release notes do SDK e o header de depreciação na resposta, se vocês tiverem um.
- Feche o ciclo. Se uma cliente pediu a mudança, ou relatou o bug que levou a ela, diga a ela quando for lançada.
Como se parece uma boa entrada de mudança que quebra algo?
Uma boa entrada nomeia o chamador afetado na primeira linha, declara a data, e inclui a correção. Aqui está uma para o caso de validação apertada, na forma que usamos:
Endereços de e-mail sem domínio são rejeitados a partir de 1º de novembro de 2026.
POST /usersePATCH /users/:idatualmente aceitam valores dealice@localhost. A partir de 1º de novembro, esses retornam400 invalid_email. Afeta qualquer integração que cria usuários a partir de diretórios internos. Migração: envie um endereço totalmente qualificado, ou omita o campo e defina-o depois. Nenhuma mudança é necessária se os endereços de vocês já têm um domínio, o que é verdade para 99,4% das contas criadas este ano.
Onde esse aviso deve viver, e o que mais deveria acompanhá-lo, é o tema de changelog de API.
A porcentagem no final não é decoração. Ela diz à leitora se ela deve se preocupar, que é a pergunta com a qual ela abriu a entrada.
Por que não simplesmente evitá-las?
Porque a alternativa é pior. Uma API que nunca quebra nada acumula todo erro que já cometeu: o campo mal nomeado, o padrão errado, o timestamp em hora local. Cada um é um imposto sobre todo novo chamador para sempre, para proteger chamadores que poderiam ter migrado em uma tarde. As equipes com a melhor reputação de estabilidade quebram coisas raramente, em um cronograma, com um caminho de migração e um aviso que alcançou as pessoas para quem era destinado.
A mecânica desse aviso é o assunto do artigo complementar sobre depreciar uma API. A entrada que a anuncia é redigida da mesma forma que qualquer outra entrada no feed de changelog: a partir do pull request mergeado, retida para um humano, depois publicada no lugar onde os chamadores afetados já leem.
FAQ
Qual é a diferença entre uma mudança que quebra algo e uma que não quebra? Uma mudança que quebra algo obriga um chamador correto a mudar seu código, sua configuração ou seus dados para continuar funcionando. Uma mudança que não quebra deixa toda chamada existente funcionando com o mesmo significado, e é por isso que adições costumam ser seguras e remoções, renomeações e regras mais apertadas costumam não ser.
Adicionar um campo obrigatório conta? Sim. Toda chamada existente o omite, então toda chamada existente falha agora. Adicione-o como opcional com um padrão sensato, ou versione o endpoint.
Uma correção de bug conta? Pode ser. Se chamadores dependiam do comportamento com bug, corrigi-lo os quebra, não importa o que a documentação dizia. Trate qualquer correção que muda a saída observável como quebrando algo, a menos que você possa mostrar que ninguém dependia dela.
Versionamento semântico se aplica a uma API web? A regra sim: mudanças que quebram algo recebem uma nova versão major e a antiga continua funcionando por um período declarado. O número frequentemente vive na URL ou em um header de data em vez de em uma versão de pacote.
Quanto aviso é suficiente? O suficiente para um chamador encontrar o aviso e fazer o trabalho. Noventa dias é um piso comum para APIs públicas; mais longo para qualquer coisa usada em código que é enviado a usuários finais e não pode ser atualizado remotamente.
As afirmações técnicas deste artigo não foram revisadas de forma independente. Se algo estiver errado, avise a gente e vamos corrigir.