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:

  1. Proxy ou gateway: roteia o tráfego entre o sistema legado e o novo, decidindo para onde cada requisição deve ir.
  2. Feature toggles: permitem ativar/desativar funcionalidades migradas sem novo deploy.
  3. 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