Strangler Fig: migrando de monolito para microserviços

1. Introdução ao Padrão Strangler Fig

O padrão Strangler Fig foi inspirado no comportamento de uma figueira estranguladora, que cresce ao redor de uma árvore hospedeira, gradualmente a envolvendo e substituindo. Na arquitetura de software, o padrão propõe uma migração incremental de um sistema monolítico para microsserviços, substituindo funcionalidades antigas por novas implementações até que o monólito original seja completamente "estrangulado".

O problema central que este padrão resolve é o risco associado a migrações "big bang", onde todo o sistema é reescrito e substituído de uma só vez. Essas abordagens frequentemente resultam em atrasos, estouro de orçamento e falhas catastróficas. O Strangler Fig oferece uma alternativa segura: a migração contínua com redução de riscos e continuidade operacional.

2. Quando Aplicar o Padrão: Cenários e Decisões Arquiteturais

O padrão é mais adequado para monólitos legados com alta complexidade e acoplamento, onde uma reescrita total seria inviável. Cenários típicos incluem:

  • Sistemas com lógica de negócio crítica que não podem sofrer interrupções
  • Aplicações monolíticas que precisam de escalabilidade independente para funcionalidades específicas
  • Equipes que desejam adotar deploy contínuo sem interromper operações existentes

Os trade-offs envolvem o custo de transformação versus os benefícios dos microsserviços. A migração incremental exige investimento contínuo em infraestrutura de roteamento, sincronização de dados e monitoramento, mas evita o risco de uma reescrita total.

3. Estrutura e Componentes do Strangler Fig

A estrutura básica do padrão envolve três componentes principais:

  • Roteador/Proxy: Ponto de interceptação que decide se uma requisição vai para o módulo legado ou para o novo microsserviço
  • Módulo legado: Sistema monolítico original que ainda executa funcionalidades não migradas
  • Novos microsserviços: Implementações modernas que substituem partes do monólito

Exemplo de configuração de proxy reverso com NGINX:

# nginx.conf
upstream monolith {
    server monolith-app:8080;
}

upstream new-service {
    server new-microservice:3000;
}

server {
    listen 80;

    location /api/users {
        # Roteamento para novo microsserviço
        proxy_pass http://new-service;
    }

    location /api/orders {
        # Roteamento para monólito legado
        proxy_pass http://monolith;
    }
}

4. Estratégias de Migração Incremental

Duas abordagens principais guiam a migração:

Abordagem por funcionalidade (feature-based): Identifica funcionalidades específicas do monólito e as substitui uma a uma. Exemplo:

# Roteamento por feature flag
if feature_flag.is_active("new-checkout"):
    return new_checkout_service(request)
else:
    return legacy_checkout_service(request)

Abordagem por domínio (bounded context): Utiliza princípios de Domain-Driven Design para identificar contextos delimitados dentro do monólito e migrá-los como microsserviços independentes.

Técnicas de estrangulamento incluem réplicas de dados temporárias e sincronização bidirecional entre o monólito e os novos serviços.

5. Desafios Técnicos e Padrões de Resiliência

O principal desafio é a consistência de dados entre sistemas. Durante a migração, dados precisam ser sincronizados entre o banco do monólito e os bancos dos microsserviços.

Padrão Saga para transações distribuídas:

# Exemplo de Saga Coreográfica
service OrderService:
    on "OrderCreated":
        PaymentService.processPayment(orderId)

service PaymentService:
    on "PaymentProcessed":
        InventoryService.reserveItems(orderId)
    on "PaymentFailed":
        OrderService.compensateOrder(orderId)

Circuit Breaker e Bulkhead isolam falhas durante a migração:

# Implementação de Circuit Breaker
if circuit_breaker.is_open("new-service"):
    return legacy_service(request)  # Fallback
else:
    try:
        return new_service(request)
    except Exception:
        circuit_breaker.record_failure()
        return legacy_service(request)

6. Estratégias de Roteamento e Roteamento Inteligente

O roteamento inteligente permite controlar o tráfego entre o monólito e os microsserviços.

Feature flags com LaunchDarkly:

# Roteamento condicional
if launchdarkly.variation("use-new-payment", user):
    return new_payment_service(request)
else:
    return legacy_payment(request)

Canary releases para validar novos serviços:

# Roteamento por percentual de tráfego
if hash(user_id) % 100 < 5:  # 5% dos usuários
    return new_service(request)
else:
    return legacy_service(request)

Dark launches executam ambos os serviços simultaneamente, comparando resultados sem impactar o usuário final.

7. Monitoramento e Validação Contínua

Métricas-chave para comparação entre monólito e microsserviço:

# Métricas comparativas
monolith_latency = 150ms
new_service_latency = 120ms

monolith_error_rate = 0.5%
new_service_error_rate = 0.3%

monolith_throughput = 1000 req/s
new_service_throughput = 1500 req/s

Rastreamento distribuído com OpenTelemetry:

# Span de rastreamento
span = tracer.start_span("process-order")
span.set_attribute("service", "new-checkout")
span.set_attribute("legacy_fallback", false)

Testes de equivalência funcional comparam saídas entre implementações legada e nova:

# Teste de equivalência
result_legacy = legacy_service(request)
result_new = new_service(request)
assert result_legacy == result_new

8. Quando Parar: Critérios de Sucesso e Encerramento

A migração é considerada completa quando:

  • Todas as funcionalidades críticas foram migradas para microsserviços
  • O tráfego para o monólito é zero ou mínimo
  • Testes de equivalência funcional são bem-sucedidos para todos os cenários

Riscos de uma migração interminável incluem:

  • Manutenção de duas bases de código simultaneamente
  • Dívida técnica acumulada no monólito durante a migração
  • Custo operacional duplicado

A decisão de manter partes do monólito é válida quando:

  • Funcionalidades são raramente alteradas
  • O custo de migração supera os benefícios
  • O acoplamento com outras partes do sistema é mínimo

Uma estratégia híbrida pode ser a melhor opção para sistemas complexos.

Referências