stamatios
← Voltar ao feed
Ponte construída ao lado de estrada antiga mantendo tráfego, migração de banco retrocompatível com expand-migrate-contract
Dev & Engenharia

Guia detalha migrações de banco retrocompatíveis com expand-migrate-contract

resumo de ~3 min

O problema da sobreposição de versões

Durante um deploy, versões antigas e novas da aplicação rodam simultaneamente: alguns servidores já executam o código novo, enquanto workers de fila, jobs agendados e clientes externos podem continuar no código antigo por tempo indeterminado. Se uma migração remove ou renomeia algo que o código antigo ainda usa, ele quebra no meio do rollout. Uma migração de banco retrocompatível é a que permite que versões antigas e novas usem o banco com segurança durante essa sobreposição, adicionando colunas, tabelas, índices, constraints ou formatos sem remover nada que o código antigo precise. O padrão recomendado divide a mudança em três fases: expand, migrate e contract.

Antes de mudar o schema

O autor alerta que uma migração que roda sem erro SQL não é automaticamente retrocompatível: o código antigo precisa continuar lendo e escrevendo dados válidos, e uma query pode rodar sem erro e ainda retornar resultado errado (por exemplo, quando uma unidade muda ou uma coluna nova tem dados incompletos). Antes de escrever a migração, é preciso mapear todos os consumidores das tabelas afetadas: servidores de aplicação e API, workers, consumidores de mensagens, jobs agendados, scripts administrativos, relatórios, stored procedures, triggers, views, réplicas, pipelines de exportação, serviços externos e versões de clientes suportadas. O cronograma de deploy define a janela de compatibilidade - um worker que roda a cada hora pode precisar da coluna antiga depois que todos os servidores web já rodarem o código novo.

Compatível não é o mesmo que reversível: alargar uma coluna de VARCHAR(10) para VARCHAR(50) pode impedir o rollback do schema quando a aplicação já gravou valores mais longos. Por isso, o autor recomenda planejar separadamente as opções de rollback - de aplicação, de tráfego, pausa do backfill, de dados e de schema - e só prometer rollback de schema se for provado que todo dado novo cabe no formato antigo sem perdas.

Expand e migrate

A fase de expand começa com adições que não quebram nada: coluna nullable, tabela nova, índice sem efeito no comportamento, view de compatibilidade. Adicionar uma coluna obrigatória de imediato costuma ser inseguro, porque o código antigo não a inclui nos inserts; o caminho mais seguro é adicioná-la como nullable, fazer o código novo preenchê-la, backfill das linhas existentes, verificar que nenhuma linha está nula e só então aplicar NOT NULL. Defaults tecnicamente válidos podem ainda estar errados para o negócio.

Durante a transição, o código deve suportar os dois formatos: ler a coluna nova quando presente e, senão, a antiga; escrever o mesmo valor nas duas. As gravações duplas devem ocorrer na mesma transação quando possível, e é preciso decidir previamente qual coluna é autoritativa em caso de divergência. Views de compatibilidade ou transformações em tempo de leitura podem substituir as gravações duplas em alguns casos, ao custo de complexidade e latência que devem ser medidas.

O backfill das linhas existentes deve rodar como job separado, e ser idempotente, reiniciável, em lotes, com throttling, observável e validado. Em tabelas grandes, uma migração pode levar horas, muito mais que um deploy. As estratégias - eager (converte tudo antes), lazy (converte ao ler/atualizar) e híbrida (backfill dos mais usados primeiro) - dependem do workload e dos SLAs, conforme pesquisa citada sobre migração em bancos NoSQL. As leituras devem mudar gradualmente: primeiro com shadow reads comparando resultados antigos e novos, depois movendo tráfego por feature flag, canário, tenants, percentual ou região, mantendo o caminho de fallback até o corte completo.

Testes, validação e contract

Cada estado intermediário precisa ser testado com uma matriz que cruza versões da aplicação (antiga/nova) e estados do banco (pré-expansão, expandido, parcialmente backfillado, totalmente backfillado, contratado). A validação vai além da contagem de linhas: nulos, unicidade, chaves estrangeiras, registros órfãos, totais agrupados, checksums e comparação de resultados antigos e novos. Conversões como dividir um campo full_name exigem regras para nomes ambíguos e revisão manual.

O monitoramento deve cobrir corretude (fallback reads, divergências entre valores antigos e novos, progresso do backfill) e carga (replication lag, lock waits, deadlocks, latência, uso de CPU e memória), com condições de parada definidas antes do deploy - por exemplo, pausar o backfill se o replication lag exceder o limite.

A fase de contract remove as estruturas antigas só quando há evidência de que nada mais as usa: todos os workers e jobs no código novo, scripts e relatórios verificados, consumidores externos migrados, backfill validado, gravações duplas sem divergências e fallback reads encerrados. Mesmo depois, vale monitorar acessos a estruturas descontinuadas, pois um worker atrasado ou relatório esquecido pode aparecer só nesse período. O autor recomenda definir uma data de remoção em vez de manter estruturas obsoletas indefinidamente, já que elas custam armazenamento, geram confusão sobre a fonte da verdade e dificultam a manutenção.