Marcio Cunha

Como mudar banco de dados em produção sem derrubar o sistema

Descubra como alterar esquemas de banco de dados em produção sem interromper a disponibilidade dos serviços usando o padrão expand contract passo a passo.

Marcio Cunha•5 min
Também disponível em:EnglishEspañol
Resumo
  • A estratégia expand contract divide alterações complexas de banco de dados em fases independentes e seguras para produção
  • Sistemas legados e aplicações modernas precisam conviver temporariamente com colunas duplicadas durante a transição
  • A fase de expansão prepara o terreno adicionando novos campos sem quebrar o código antigo que ainda roda no ar
  • A fase de contratos garante a estabilidade retroativa antes de remover o código obsoleto e os campos antigos
  • O monitoramento contínuo de logs e métricas evita surpresas desagradáveis durante a virada definitiva das chaves

Por que alterar banco de dados em produção costuma dar dor de cabeça?

Quem trabalha com engenharia de software sabe que alterar o esquema de um banco de dados em produção costuma ser uma tarefa cercada de apreensão. Em sistemas de alta disponibilidade, parar o serviço por alguns minutos para rodar um comando de alteração de tabela significa perder receita, frustrar usuários e gerar chamados de suporte urgentes. Na prática, isso acontece porque as aplicações e os bancos conversam em uma linguagem rígida, onde qualquer mudança estrutural repentina faz o código antigo cuspir erros em cadeia.

Para contornar esse problema sem precisar exibir telas de manutenção na madrugada, os arquitetos de software adotam estratégias baseadas em versionamento gradual e compatibilidade retroativa. Esse cuidado é o mesmo que aplicamos quando planejamos como migrar bases de dados de um monolito sem downtime, garantindo que o tráfego real continue fluindo enquanto a fundação do sistema é trocada por baixo. O segredo consiste em separar a mudança física dos dados da alteração lógica do código da aplicação.

O que é e como funciona o padrão expand contract na prática?

O padrão expand contract consiste em dividir qualquer alteração complexa de banco de dados em três etapas distintas: expansão, transição e contrato. Na primeira fase, chamada de expansão, você altera o banco apenas para adicionar novos elementos, como uma nova coluna ou tabela, sem remover ou modificar o que já existe. Dessa forma, a aplicação atual continua funcionando perfeitamente porque nada do que ela usa foi quebrado ou alterado de lugar.

Na fase intermediária, você atualiza a aplicação para começar a escrever dados tanto no formato antigo quanto no novo formato simultaneamente. Por fim, na fase de contrato, depois que todo o código já foi atualizado e os dados antigos foram migrados em segundo plano, você remove o esquema antigo com total segurança. Esse fluxo garante que o sistema nunca fique indisponível, pois em nenhum momento o banco de dados deixa de atender às requisições que a aplicação espera receber.

Passo a passo para aplicar a expansão sem interromper o serviço

O primeiro passo prático para aplicar essa técnica é criar a nova estrutura no banco de dados sem tocar no código existente. Imagine que você precisa renomear a coluna full_name para first_name e last_name em uma tabela de usuários muito acessada. Se você apenas alterar a coluna diretamente, o sistema atual vai falhar ao tentar buscar um campo que sumiu.

Para resolver isso, você executa um comando SQL simples para adicionar as novas colunas permitindo valores nulos, conforme o exemplo abaixo:

ALTER TABLE users ADD COLUMN first_name VARCHAR(100);
ALTER TABLE users ADD COLUMN last_name VARCHAR(100);

Com esse comando executado, o banco ganha novos espaços para trabalhar, mas a aplicação continua usando o campo antigo full_name sem perceber nenhuma mudança no ambiente de produção.

Atualizando o código da aplicação com escrita dupla

Com as novas colunas disponíveis no banco de dados, o segundo passo exige alterar o código da aplicação para adotar a estratégia de escrita dupla. Na prática, sempre que um usuário se cadastrar ou atualizar seu perfil, o sistema vai preencher tanto a coluna antiga quanto as novas colunas recém-criadas. Esse comportamento garante que a informação fique redundante e segura em ambos os lugares durante o período de transição.

Para garantir que consultas complexas continuem performáticas durante essa fase de duplicação, vale a pena revisar a indexação das tabelas afetadas, seguindo preceitos semelhantes aos que discutimos ao analisar como otimizar consultas com índices compostos. Um bom índice evita que a escrita dupla gere gargalos de desempenho e mantenha a resposta da API rápida para o usuário final.

Abaixo temos um exemplo ilustrativo em código que demonstra como realizar essa inserção dupla de forma segura na camada de serviço da sua aplicação:

async function updateUserProfile(userId, fullName) {
const parts = fullName.split(' ');
const firstName = parts[0];
const lastName = parts.slice(1).join(' ');

await db.query(
'UPDATE users SET full_name = $1, first_name = $2, last_name = $3 WHERE id = $4',
[fullName, firstName, lastName, userId]
);
}

Migrando o histórico de dados legados em segundo plano

Enquanto a aplicação nova escreve nos dois lugares, os registros antigos que já estavam no banco continuam apenas com o dado preenchido na coluna antiga. Para resolver essa lacuna sem travar o sistema, você deve rodar um script de migração em segundo plano, muitas vezes chamado de backfill. Esse script lê os registros em lotes pequenos e vai preenchendo as novas colunas aos poucos, respeitando os limites de consumo de CPU e disco do servidor.

Essa abordagem evita que o banco de dados trave por excesso de bloqueios em tabelas gigantescas. Quando o script termina de varrer toda a tabela, significa que 100% dos dados históricos já possuem o formato novo populado e pronto para ser consumido pelas próximas versões do software.

Contratando e removendo o código antigo com segurança

A última etapa do processo é o contrato, momento em que você limpa a sujeira deixada pelas fases anteriores. Depois que o código da aplicação foi atualizado para ler exclusivamente das novas colunas first_name e last_name, e o script de migração terminou o seu trabalho, a coluna antiga full_name deixa de ter utilidade prática.

Nesse ponto, você pode realizar uma nova alteração no banco de dados para remover a coluna antiga e limpar o código legado da aplicação com total tranquilidade. Essa disciplina operacional transforma uma tarefa estressante em uma rotina previsível de engenharia, permitindo entregas contínuas de valor sem sacrificar a estabilidade do negócio. Para aprofundar esses conceitos na prática em produção, confira também nossa análise técnica sobre Como monitorar containers em tempo real com eBPF no Linux.

Considerações finais sobre a migração sem paradas

Adotar o padrão expand contract exige disciplina da equipe de engenharia, pois transformamos uma tarefa que seria feita em um único comando arriscado em um processo dividido em etapas controladas. Embora exija mais linhas de código temporárias, o ganho em termos de resiliência e paz de espírito compensa amplamente o esforço adicional.

Ao dominar essa técnica, sua equipe ganha autonomia para evoluir o modelo de dados de sistemas complexos a qualquer momento do dia, sem depender de janelas de manutenção noturnas ou finais de semana prolongados. O resultado é um produto mais ágil, confiável e preparado para crescer de forma sustentável.