Todo sistema legado chega no mesmo ponto. Uma feature simples leva três semanas. Ninguém entende mais o módulo de faturamento. O único dev que conhecia aquela parte saiu em 2019. E então alguém diz a frase: "é mais rápido reescrever do zero".
Quase nunca é.
Por que o rewrite big bang falha
A proposta parece racional: time paralelo, stack moderna, seis meses, e no dia D vira a chave. O problema é que essa aposta carrega quatro riscos que se multiplicam entre si.
O alvo se move. Enquanto você reescreve, o sistema antigo continua recebendo correções e features — porque o negócio não para. Ou você congela o produto por seis meses (inaceitável), ou implementa cada mudança duas vezes, em dois sistemas diferentes.
O escopo é desconhecido. O legado tem comportamento que ninguém documentou e que virou regra de negócio por acidente. Aquele if estranho no cálculo de frete existe porque um cliente grande pediu em 2017. Você só descobre isso depois de migrar — quando ele liga.
Não há entrega parcial. Você queima seis meses de orçamento sem colocar nada em produção. Se o projeto for cancelado no mês cinco, o valor entregue é exatamente zero.
O dia D é tudo ou nada. Um único deploy troca o sistema inteiro. Se der errado, o rollback é do sistema inteiro — sob pressão, num sábado de madrugada.
O padrão que se repete: o rewrite atrasa, o legado continua evoluindo, a distância entre os dois aumenta, e no fim a empresa mantém dois sistemas em produção. O pior dos dois mundos.
A figueira que estrangula
Martin Fowler batizou o padrão em 2004 depois de ver figueiras mata-pau na Austrália. A semente germina num galho da árvore hospedeira, joga raízes para baixo até o chão e vai envolvendo o tronco. Anos depois, a hospedeira morre e apodrece — e o que sobra é uma figueira oca, com a forma exata da árvore que substituiu.
Aplicado a software: você não desliga o legado, você o envolve. Cada pedaço novo entra em produção assim que fica pronto, atendendo tráfego real. O legado vai perdendo responsabilidades até não sobrar nada — e aí você o remove.
| Big bang | Strangler Fig | |
|---|---|---|
| Primeira entrega em produção | No fim do projeto | Em semanas |
| Rollback | Do sistema inteiro | De uma rota |
| Se o projeto parar no meio | Nada entregue | A parte migrada continua valendo |
| Regras de negócio ocultas | Descobertas no dia D | Descobertas uma rota por vez |
| Custo total | Menor no papel | Maior — proxy e convivência custam |
Repare na última linha. Strangler Fig não é mais barato. Você paga por infraestrutura de roteamento, por manter dois sistemas conversando e por sincronização de dados. O que você compra com esse custo é a eliminação do risco de tudo-ou-nada.
A peça central: o proxy
Tudo depende de uma coisa: interceptar o tráfego antes que ele chegue no legado. Esse ponto de intercepção é o que permite mover uma rota de cada vez.
Na forma mais simples, é configuração de nginx:
upstream legado { server legado.interno:8080; }
upstream novo { server novo.interno:3000; }
server {
listen 80;
# Já migrado → serviço novo
location /api/clientes { proxy_pass http://novo; }
location /api/enderecos { proxy_pass http://novo; }
# Todo o resto continua no legado
location / { proxy_pass http://legado; }
}
Migrar uma rota vira mover uma linha de configuração. Fazer rollback também. Esse é o ponto inteiro do padrão: a decisão de migrar deixa de ser um evento e vira uma operação reversível.
Quando você precisa de mais controle — rollout percentual, feature flags por cliente, comparação de respostas — o proxy vira código:
const ROTAS_MIGRADAS = [
{ padrao: /^\/api\/clientes/, percentual: 100 },
{ padrao: /^\/api\/pedidos/, percentual: 10 }, // rollout gradual
];
function escolherDestino(req) {
const rota = ROTAS_MIGRADAS.find(r => r.padrao.test(req.path));
if (!rota) return LEGADO;
// Hash estável do usuário: o mesmo usuário sempre cai no mesmo lado.
// Sem isso, o cliente alterna entre sistemas a cada request.
const bucket = hashEstavel(req.user?.id ?? req.ip) % 100;
return bucket < rota.percentual ? NOVO : LEGADO;
}
Detalhe que quebra em produção: se o roteamento for aleatório em vez de determinístico por usuário, o mesmo cliente vai bater ora no sistema novo, ora no legado. Com estados diferentes nos dois lados, ele vê dados aparecerem e sumirem. Sempre use hash estável de um identificador do usuário.
Escolhendo a primeira fatia
A primeira migração define se o projeto ganha ou perde credibilidade interna. Ela precisa entregar valor visível e provar o mecanismo — sem arriscar o faturamento.
Michael Feathers chama de costura (seam) o ponto onde dá para alterar o comportamento do sistema sem editar o código naquele lugar. Uma boa primeira costura tem quatro características:
- Fronteira nítida — o módulo conversa com o resto por poucas chamadas bem definidas, não por trinta pontos espalhados
- Poucas tabelas compartilhadas — quanto menos dados em comum com o legado, menor o problema de sincronização
- Leitura predominante — rotas de escrita trazem o problema de consistência junto; comece pelas de leitura
- Dor real — algo que já dá manutenção ou está lento, para a migração ter justificativa de negócio
O que não escolher primeiro: autenticação, faturamento, fechamento contábil. São o núcleo transacional, têm as regras mais obscuras e o custo de errar é o mais alto. Eles vêm por último, quando você já domina o mecanismo.
O problema difícil são os dados
Rotear requisição é a parte fácil. O que realmente decide o sucesso da migração é o banco — e é aqui que a maioria dos projetos trava.
Enquanto legado e sistema novo coexistem, algum dado precisa estar visível para os dois. Existem três caminhos, em ordem crescente de custo e de correção.
1. Banco compartilhado
Os dois sistemas leem e escrevem na mesma base. É o mais simples de começar e o mais comum na prática.
O custo é que o schema vira contrato entre dois sistemas: você não pode renomear uma coluna sem quebrar o outro lado. Aceitável como estágio temporário, com a extração do banco planejada. Vira dívida quando o "temporário" dura três anos.
2. Dual write (evite)
A aplicação escreve nos dois bancos a cada operação. Parece resolver, mas tem uma falha estrutural:
await bancoLegado.salvar(pedido); // ✓ commitou
await bancoNovo.salvar(pedido); // ✗ timeout de rede
Não existe transação atômica entre dois bancos distintos. Quando a segunda escrita falha, os dados divergem silenciosamente — e você só descobre semanas depois, quando um relatório não bate. Reconciliar isso manualmente é caro e demorado.
3. Outbox transacional ou CDC
A abordagem correta: uma única transação grava o dado e o registro da mudança na mesma base. Um processo separado lê esses registros e propaga.
BEGIN;
INSERT INTO pedidos (id, cliente_id, total)
VALUES ('p-1', 'c-9', 250.00);
-- Mesma transação: se der rollback, o evento some junto
INSERT INTO outbox (agregado, evento, payload)
VALUES ('pedido', 'PedidoCriado',
'{"id":"p-1","cliente_id":"c-9","total":250.00}');
COMMIT;
Se a transação falhar, o dado e o evento somem juntos — nunca ficam inconsistentes. O consumidor da outbox propaga para o outro sistema com retry, e a propagação é idempotente: reprocessar o mesmo evento não duplica nada.
A alternativa sem tocar no código do legado é CDC (Change Data Capture): ferramentas como Debezium leem o log de replicação do banco e publicam as mudanças como eventos. Útil justamente quando alterar o legado é caro ou arriscado.
Nos três casos a consistência é eventual: existe uma janela em que os dois lados divergem. Isso precisa ser decisão consciente do negócio, não descoberta do time de suporte. Para saldo financeiro, a janela pode ser inaceitável — e aí aquela fatia não migra ainda.
Como saber que o código novo está certo
Testes cobrem o comportamento que você conhece. O risco no legado é justamente o comportamento que ninguém documentou.
A técnica que resolve isso é o parallel run: por um período, toda requisição vai para os dois sistemas. A resposta do legado é a que volta para o usuário; a do sistema novo é apenas comparada e registrada.
async function calcularFrete(pedido) {
const legado = await freteLegado(pedido);
// Não bloqueia e não pode derrubar a requisição
Promise.resolve()
.then(async () => {
const novo = await freteNovo(pedido);
if (!equivalente(legado, novo)) {
logger.warn('divergencia_frete', {
pedidoId: pedido.id, legado, novo
});
}
})
.catch(err => logger.error('parallel_run_falhou', err));
return legado; // usuário recebe sempre a resposta do legado
}
Depois de algumas semanas, o log de divergências é o documento que ninguém escreveu: cada divergência é uma regra de negócio oculta ou um bug do código novo. Quando a taxa chega perto de zero em volume real, você inverte a origem da resposta com confiança de dados, não de opinião.
Dois cuidados: o parallel run dobra a carga naquela rota, então avalie a capacidade antes; e ele só serve para operações sem efeito colateral. Nunca rode em paralelo algo que cobra cartão, envia e-mail ou baixa estoque — a menos que o caminho novo esteja em modo simulado.
Quando terminar
A migração acaba quando o proxy não tem mais nenhuma rota apontando para o legado. Aí vêm as duas últimas etapas, e elas são as mais puladas:
- Desligar o legado, mas mantê-lo de pé por algumas semanas. Sem tráfego, mas ligável. É a sua apólice de seguro.
- Remover de verdade — código, servidores, jobs agendados, credenciais, acessos. Legado desligado que continua existindo volta a ligar sozinho um dia.
O proxy costuma ficar. Ele já é o ponto natural para autenticação, rate limiting e observabilidade — a migração acabou entregando um API gateway de brinde.
Quando não usar
O padrão não é universal. Ele cobra um preço em complexidade, e há casos em que esse preço não compensa:
- Sistema pequeno. Se dá para reescrever em duas ou três semanas, o overhead de proxy e sincronização é maior que o rewrite. Reescreva.
- Não existe ponto de intercepção. Desktop monolítico, banco acessado direto pelo cliente, integrações que apontam para o IP do servidor. Sem lugar para o proxy, não há padrão — o primeiro trabalho é criar essa costura.
- O domínio vai mudar. Se as regras de negócio serão redesenhadas do zero, não há o que estrangular: o sistema novo não é substituto do antigo, é outro produto.
Em resumo
Strangler Fig não é a opção mais rápida nem a mais barata. É a que permite errar barato: cada passo entra em produção sozinho, prova seu valor com tráfego real e volta atrás com uma linha de configuração.
Num rewrite big bang, você descobre se acertou depois de seis meses. Aqui, você descobre na primeira semana — e ainda dá tempo de corrigir o rumo.