Padrão strangler fig: migrando sistemas legados gradualmente
1. Fundamentos do Padrão Strangler Fig
O padrão Strangler Fig (figueira estranguladora) recebe esse nome por analogia com uma árvore tropical que cresce sobre outra árvore hospedeira, gradualmente substituindo sua estrutura até que a original morra. Na engenharia de software, o conceito é idêntico: um novo sistema cresce ao redor do legado, interceptando chamadas e funcionalidades até que o antigo possa ser completamente removido.
O objetivo principal é substituir sistemas legados sem interrupção total do serviço. Diferente da abordagem "big bang" — onde o sistema antigo é desligado e o novo entra em produção simultaneamente, com altíssimo risco de falhas catastróficas — a migração incremental permite que cada funcionalidade seja substituída, testada e validada individualmente.
2. Quando Aplicar o Strangler Fig
Sinais claros de que um sistema legado precisa ser substituído incluem:
- Dificuldade crescente para implementar novas funcionalidades
- Tempo de deploy excessivo (dias ou semanas)
- Falta de documentação e conhecimento concentrado em poucas pessoas
- Custos operacionais desproporcionais ao valor gerado
O Strangler Fig é ideal quando:
- O sistema legado é crítico para o negócio e não pode ficar offline
- Existem múltiplas funcionalidades que podem ser isoladas
- A equipe pode dedicar esforço contínuo à migração
Os riscos mitigados incluem continuidade de negócios (nunca há parada total) e redução gradual da dívida técnica, permitindo que o time aprenda com cada iteração.
3. Arquitetura e Componentes do Padrão
A arquitetura típica envolve três componentes principais:
- Proxy ou gateway: roteia o tráfego entre o sistema legado e o novo, decidindo para onde cada requisição deve ir.
- Feature toggles: permitem ativar/desativar funcionalidades migradas sem novo deploy.
- Anti-corruption layer: adaptadores que traduzem dados entre o modelo antigo e o novo.
A estrutura segue três fases:
- Interceptação: o proxy começa a capturar chamadas específicas
- Redirecionamento: funcionalidades selecionadas são enviadas ao novo sistema
- Remoção gradual: partes do legado são desativadas conforme o novo sistema assume
4. Estratégias de Migração Passo a Passo
Identificação de funcionalidades independentes: comece por funcionalidades com baixo acoplamento, como módulos de relatório ou busca.
Implementação de adaptadores: crie uma camada que isola o sistema legado das mudanças:
// Adaptador entre banco legado (SQL antigo) e novo serviço (API REST)
class AntiCorruptionLayer {
constructor(legacyDb, newService) {
this.legacyDb = legacyDb;
this.newService = newService;
}
async getUser(userId) {
if (featureFlags.isUserMigrated(userId)) {
// Busca no novo sistema
return await this.newService.fetchUser(userId);
} else {
// Busca no legado e converte formato
const legacyData = await this.legacyDb.query(
'SELECT * FROM usuarios WHERE id = ?', [userId]
);
return this.convertLegacyToNew(legacyData);
}
}
convertLegacyToNew(legacyData) {
return {
id: legacyData.id,
name: legacyData.nome,
email: legacyData.email_contato,
createdAt: legacyData.data_cadastro
};
}
}
Estratégias de roteamento:
- Por funcionalidade: "/api/relatorios" vai para o novo, "/api/faturamento" fica no legado
- Por usuário: usuários beta testam o novo sistema
- Por dados: registros criados após data X vão para o novo banco
5. Exemplo Prático em Node.js
Configuração de proxy reverso com Express para roteamento gradual:
const express = require('express');
const httpProxy = require('http-proxy-middleware');
const app = express();
const proxy = httpProxy.createProxyMiddleware({
target: 'http://sistema-legado:3000',
changeOrigin: true
});
// Roteamento por funcionalidade
app.use('/api/relatorios', (req, res, next) => {
const novoDestino = 'http://novo-sistema:4000';
httpProxy.createProxyMiddleware({
target: novoDestino,
changeOrigin: true
})(req, res, next);
});
// Roteamento por usuário (via header)
app.use('/api/usuarios', (req, res, next) => {
const userId = req.headers['x-user-id'];
if (featureFlags.isUserMigrated(userId)) {
const novoDestino = 'http://novo-sistema:4000';
httpProxy.createProxyMiddleware({
target: novoDestino,
changeOrigin: true
})(req, res, next);
} else {
proxy(req, res, next);
}
});
// Demais rotas vão para o legado
app.use('/', proxy);
app.listen(8080, () => {
console.log('Gateway Strangler Fig rodando na porta 8080');
});
Implementação de um adaptador entre banco legado e novo serviço:
class OrderMigrationAdapter {
constructor() {
this.legacyOrders = {}; // cache temporário
}
async createOrder(orderData) {
if (featureFlags.isOrderMigrationComplete()) {
// Novo sistema
const response = await fetch('http://novo-sistema:4000/api/pedidos', {
method: 'POST',
body: JSON.stringify(orderData),
headers: { 'Content-Type': 'application/json' }
});
return response.json();
} else {
// Sistema legado (simulação)
const id = Object.keys(this.legacyOrders).length + 1;
this.legacyOrders[id] = {
...orderData,
id,
created_at: new Date().toISOString()
};
return this.legacyOrders[id];
}
}
async getOrder(orderId) {
// Tenta novo sistema primeiro
try {
const response = await fetch(`http://novo-sistema:4000/api/pedidos/${orderId}`);
if (response.ok) return response.json();
} catch (e) {
// Fallback para legado
}
return this.legacyOrders[orderId] || null;
}
}
6. Desafios e Boas Práticas
Gerenciamento de estado: durante a migração, dados podem existir em ambos os sistemas. Use sincronização em lote ou eventos para manter consistência.
Testes de rollback: cada iteração deve ter um plano claro de reversão. Mantenha o legado intacto até que o novo sistema esteja completamente validado.
Monitoramento: instrumente cada chamada roteada:
// Middleware de observabilidade
app.use((req, res, next) => {
const start = Date.now();
const destino = req.headers['x-routed-to'] || 'legado';
res.on('finish', () => {
console.log(`[${destino}] ${req.method} ${req.url} - ${res.statusCode} (${Date.now() - start}ms)`);
});
next();
});
7. Relação com Padrões Vizinhos
Strangler Fig vs. Monolito Modular: ambos decompõem sistemas, mas o Strangler Fig foca na substituição externa, enquanto o Monolito Modular reorganiza internamente.
Integração com CQRS: durante a migração, use comandos no novo sistema e consultas no legado (ou vice-versa), separando responsabilidades.
Competing Consumers: para migração de filas, processe mensagens tanto no legado quanto no novo sistema até que o antigo seja desligado.
8. Conclusão e Próximos Passos
Checklist para planejamento:
- [ ] Mapear todas as funcionalidades do sistema legado
- [ ] Priorizar funcionalidades com menor acoplamento
- [ ] Implementar proxy/gateway com feature toggles
- [ ] Criar anti-corruption layer para cada módulo
- [ ] Definir métricas de sucesso (tempo de resposta, taxa de erro)
- [ ] Estabelecer procedimentos de rollback
Métricas de sucesso incluem redução de downtime (idealmente zero), aumento de confiabilidade e velocidade de entrega. Considere abandonar o padrão se o custo de manutenção do adaptador superar o benefício da migração gradual — nesse caso, um rewrite total pode ser mais econômico.
Referências
- Martin Fowler — StranglerFigApplication — Artigo original de Martin Fowler que define o padrão e suas motivações
- Microsoft — Strangler Fig pattern — Documentação oficial da Microsoft sobre implementação do padrão em arquiteturas cloud
- AWS — Migrating legacy applications using the Strangler Fig pattern — Guia prático da AWS com exemplos de migração para microsserviços
- ThoughtWorks — Strangler Fig Application — Experiência prática da ThoughtWorks com o padrão em projetos reais
- O'Reilly — Migrating to Microservices with the Strangler Fig Pattern — Capítulo de livro abordando estratégias detalhadas de migração