Uma alteração de banco parece simples enquanto cabe numa linha de SQL. Renomear uma coluna, adicionar uma restrição ou mudar o formato de um dado vira outro problema quando duas versões da aplicação podem rodar ao mesmo tempo. Durante um deploy gradual, o código antigo ainda espera o schema antigo enquanto o novo já tenta usar o próximo.
O padrão expand-and-contract, também chamado de parallel change, troca a virada única por etapas compatíveis. Primeiro o sistema aprende o formato novo sem perder o antigo. Depois os dados e consumidores migram. Só quando ninguém depende da estrutura anterior ela é removida.
O deploy perigoso é o tudo ou nada
Imagine uma tabela customers com a coluna name. O novo produto quer separar given_name e family_name. Renomear ou apagar name no mesmo deploy que altera o código cria uma janela frágil. Se uma instância antiga receber tráfego, ela quebra. Se for necessário rollback, a versão anterior encontra um banco que não entende mais.
A compatibilidade temporária é o que preserva o rollback. Enquanto o schema aceita os dois contratos, a equipe consegue recuar a aplicação sem precisar desfazer uma migração destrutiva às pressas.
1. Expandir
Na expansão, adicione a nova estrutura de forma compatível. Colunas novas costumam começar aceitando NULL ou um valor seguro, porque os registros existentes ainda não foram migrados. O código antigo continua usando name.
ALTER TABLE customers ADD COLUMN given_name text;
ALTER TABLE customers ADD COLUMN family_name text;
O impacto operacional do comando depende do banco, da versão, do tamanho da tabela e da alteração. Algumas operações exigem locks ou reescrita. Verifique o plano na documentação do seu SGBD e ensaie com um volume próximo ao real; “é só um ALTER TABLE” já derrubou deploy suficiente para merecer respeito.
2. Migrar
A próxima versão passa a escrever nos campos antigos e novos, ou usa uma camada de compatibilidade centralizada. Depois um backfill copia registros históricos em lotes pequenos. Métricas acompanham quantos registros ainda não possuem o formato novo e se as duas representações divergem.
Dual write parece trivial, mas pode falhar pela metade. Se duas gravações independentes forem feitas fora da mesma transação, uma pode confirmar e a outra não. Quando possível, mantenha a atualização atômica no banco. Se a migração atravessa serviços ou armazenamentos diferentes, trate reconciliação e idempotência como partes do desenho.
Quando o backfill termina, o código novo começa a ler as colunas novas, ainda mantendo a escrita compatível por um período. Essa fase de observação permite comparar resultados, testar rollback e encontrar consumidores esquecidos, como jobs, relatórios e integrações.
3. Contrair
A contração acontece somente depois que telemetria e busca no código mostram que ninguém lê ou escreve o campo antigo. Primeiro remova o caminho antigo da aplicação. Em outro deploy, retire a coluna. Restrições como NOT NULL podem ser aplicadas quando os dados já foram validados.
Essa distância entre “parou de usar” e “apagou” parece lenta, mas é o preço barato. A alternativa é descobrir um consumidor oculto depois que a informação já não existe.
O que observar em cada fase
- Tempo de lock e crescimento de latência durante mudanças de schema.
- Taxa e duração do backfill, com pausa possível em caso de pressão.
- Divergências entre representações antigas e novas.
- Versões da aplicação ainda acessando o contrato antigo.
- Plano de rollback válido antes de cada passo destrutivo.
Zero downtime não significa risco zero
Sistemas enormes usam snapshots, replicação e troca de tráfego para mover dados sem interrupção perceptível. Um CRUD comum não precisa copiar a arquitetura da Stripe, mas pode aproveitar o princípio: separar preparação, cópia, validação e corte torna a mudança observável e reversível.
O padrão também serve para APIs, eventos e mudanças de formato. Sempre que produtores e consumidores não atualizam no mesmo instante, expandir o contrato antes de remover o antigo reduz coordenação e sustos.
A tela de manutenção continua válida quando o contexto aceita parada e o custo de engenharia não se justifica. Expand-and-contract entra quando disponibilidade, múltiplas instâncias ou rollback importam. Ele não faz a migração desaparecer; transforma uma aposta grande em uma sequência de passos verificáveis.
Backfill precisa de checkpoint e limite de pressão
Não atualize a tabela inteira numa transação. Processe lotes por chave estável, confirme cada lote e grave o último checkpoint. Limite concorrência e pause quando réplica, I/O ou latência ultrapassarem o orçamento.
UPDATE customers
SET normalized_email = lower(email)
WHERE id > :last_id
AND id <= :next_id
AND normalized_email IS NULL;
O processo deve poder repetir o mesmo intervalo sem corromper resultado. Essa idempotência operacional permite retomar depois de deploy, timeout ou falha do worker. Para efeitos distribuídos, a disciplina se conecta à transactional outbox e à idempotência da API.
Cada fase precisa de uma saída de emergência diferente
- Expand: remover o código novo e deixar a coluna sem uso costuma ser reversível.
- Dual write: desligar a escrita nova exige saber se o dado antigo continua completo.
- Leitura nova: voltar à leitura antiga só funciona enquanto os dois formatos permanecem coerentes.
- Contract: depois de apagar coluna ou restrição antiga, rollback pode exigir restauração ou nova migração.
Use uma feature flag para mover a leitura por percentuais quando a aplicação permite, mas não use a flag para esconder uma migração destrutiva. Antes de contrair, confirme que nenhum binário antigo, job, relatório ou consumidor externo ainda lê o contrato anterior.
Registre contagem de linhas antigas e novas, divergências, duração dos lotes e atraso de réplica. A fase termina por evidência, não porque “já passou uma semana sem reclamação”.

