MÓDULO 15 · CONCEITO 07 DE 12

Strangler Fig & migração de sistemas legados

Martin Fowler 2004 — roteamento progressivo, Anti-Corruption Layer, dual write, shadow mode, e quando a reescrita completa é mais honesta

Tempo de leitura ~19 min Pré-requisito 06 · Event-Driven Architecture Próximo 08 · DDD Tático I

Em 2004, Martin Fowler publicou um artigo descrevendo o padrão que chamou de Strangler Fig Application. O nome vem da figueira-estranguladora: uma planta parasita que envolve uma árvore existente, cresce lentamente ao redor dela, e eventualmente a substitui por completo enquanto a árvore original vai morrendo. A metáfora é precisa: você constrói o sistema novo ao redor do sistema existente, transfere responsabilidades gradualmente, e eventualmente o sistema legado pode ser desligado — sem que tenha havido um big bang, sem que o sistema tenha ficado fora do ar.

A alternativa ao Strangler Fig é a reescrita completa ("big bang rewrite"). Joel Spolsky, em 2000, chamou isso de "o único erro grave que uma empresa de software pode cometer". A crítica é justa: reescritas completas falham em padrão reconhecível — o sistema novo fica preso em "quase pronto" por meses ou anos, enquanto o sistema legado ainda carrega produção e ainda recebe bugs que precisam ser portados para o sistema novo. Mas reescritas completas não são sempre erradas — às vezes são a opção mais honesta.

A mecânica do Strangler Fig

O padrão tem três componentes fundamentais:

1. O proxy (ou façade) de roteamento. Todo tráfego entra por um ponto único — um proxy, um API gateway, um load balancer — que decide se roteia para o sistema legado ou para o sistema novo. No início, 100% vai para o legado. À medida que o novo sistema assume responsabilidades, o roteamento muda: "requests para /orders/v2 vão para o novo serviço; requests para /orders vão para o legado por enquanto".

2. O Anti-Corruption Layer (ACL). O sistema novo precisa se comunicar com o sistema legado durante a transição — talvez ler dados que ainda estão no banco legado, talvez acionar operações que ainda estão implementadas lá. O ACL é a camada que traduz: o novo sistema fala sua própria linguagem; o ACL traduz para a linguagem do legado. Sem o ACL, o novo sistema acaba poluído pelo modelo de dados do legado — e você não tem mais um sistema novo, tem um legado reescrito na nova tecnologia com os mesmos problemas.

3. O corte progressivo (feature flags ou versioning). Responsabilidades são transferidas uma por uma, com a capacidade de reverter. Feature flags permitem que o roteamento seja controlado em produção sem deploy, o que é especialmente valioso quando o comportamento do sistema novo precisa ser validado com tráfego real antes do corte total.

Técnicas de migração de dados

A parte mais difícil de qualquer migração de sistema legado não é o código — é os dados. Dois sistemas em paralelo precisam de estratégia para manter dados sincronizados.

Dual write. Durante a transição, o sistema que recebe writes escreve em ambos os bancos — legado e novo. Isso garante que os dois sistemas têm os dados atualizados. O risco é inconsistência: se a escrita no novo banco falha após o legado ter sido atualizado, os dados divergem. Mitigação: saga com compensating action, ou idempotent retry com reconciliação periódica.

Change Data Capture (CDC). Em vez de dual write na aplicação, você captura as mudanças no banco legado (via WAL do PostgreSQL, binlog do MySQL, Debezium) e as replica para o novo banco. Mais confiável que dual write manual, mas requer infraestrutura adicional.

Backfill + shadow mode. Você faz backfill dos dados históricos para o novo banco, depois coloca o novo sistema em shadow mode: ele processa todos os requests mas não retorna resposta (apenas registra). Você compara as respostas do novo sistema com as do legado em silêncio, corrigi divergências, e só faz o corte quando as respostas batem.

princípio do shadow mode

Shadow mode é a técnica mais segura para validar comportamento do novo sistema com tráfego real sem risco para o usuário. O novo sistema processa tudo mas não responde; um comparador registra diferenças. Só faça o corte quando a taxa de divergência for aceitável (idealmente zero para lógica crítica).

O Anti-Corruption Layer em detalhes

O ACL é um dos padrões mais importantes do DDD estratégico, e aparece naturalmente em migrações. A ideia é criar uma camada que traduz entre dois modelos de domínio diferentes — neste caso, o modelo do sistema legado e o modelo do sistema novo.

Sem o ACL, a tentação é usar os tipos e estruturas do legado diretamente no novo sistema. O resultado é o novo sistema poluído pelo vocabulário do legado: campos com nomes que fazem sentido apenas no contexto histórico do legado, estruturas que refletem limitações técnicas antigas, lógica que existe apenas para compatibilidade com bugs conhecidos do sistema antigo.

Com o ACL, o novo sistema define seu modelo limpo e o ACL traduz quando precisa comunicar com o legado. Quando o legado for desligado, o ACL também vai embora — e o modelo do novo sistema permanece limpo.

C# — LegacyOrderGateway como ACL, OrderRouter com feature flag
// Novo sistema — modelo limpo
public record Order(
    OrderId Id,
    CustomerId CustomerId,
    IReadOnlyList<OrderLine> Lines,
    OrderStatus Status
);

// Anti-Corruption Layer — traduz entre novo e legado
public class LegacyOrderGateway
{
    private readonly LegacyDatabase _legacyDb;

    // Novo sistema chama isso sem saber do legado
    public async Task<Order?> GetOrderAsync(OrderId id, CancellationToken ct)
    {
        // Legado tem schema diferente: tb_pedidos com campos legados
        var legacyRow = await _legacyDb.QueryAsync(
            "SELECT cod_pedido, cod_cliente, status_interno FROM tb_pedidos WHERE cod_pedido = @id",
            new { id = id.Value }, ct
        );

        if (legacyRow is null) return null;

        // Tradução: modelo legado → modelo novo
        return new Order(
            Id: new OrderId(legacyRow.CodPedido),
            CustomerId: new CustomerId(legacyRow.CodCliente),
            Lines: await GetLegacyLinesAsync(legacyRow.CodPedido, ct),
            Status: TranslateStatus(legacyRow.StatusInterno) // vocabulário diferente
        );
    }

    private static OrderStatus TranslateStatus(string legacyStatus) => legacyStatus switch
    {
        "PE" => OrderStatus.Pending,    // "PE" = "Pendente" no legado
        "AP" => OrderStatus.Confirmed,  // "AP" = "Aprovado" no legado
        "EN" => OrderStatus.Shipped,    // "EN" = "Enviado" no legado
        _ => throw new InvalidOperationException($"Status legado desconhecido: {legacyStatus}")
    };
}

// Proxy de roteamento — decide qual backend usar
public class OrderRouter
{
    private readonly INewOrderService _newService;
    private readonly ILegacyOrderService _legacyService;
    private readonly IFeatureFlagService _flags;

    public async Task<Order?> GetAsync(OrderId id, CancellationToken ct)
    {
        // Roteamento gradual: orders acima de um threshold vão para o novo sistema
        if (_flags.IsEnabled("new-order-service") && id.Value > NewSystemCutoff)
            return await _newService.GetOrderAsync(id, ct);

        return await _legacyService.GetOrderAsync(id, ct); // legado ainda serve o resto
    }
}

TranslateStatus mapeia o vocabulário do legado para o novo sistema. Quando o legado for desligado, o ACL vai junto — sem poluição no modelo novo.

Python — ShadowOrderService para validar o novo sistema em silêncio
from dataclasses import dataclass
from enum import Enum
from typing import Optional

# Novo modelo — limpo, sem contaminação do legado
class OrderStatus(Enum):
    PENDING = "pending"
    CONFIRMED = "confirmed"
    SHIPPED = "shipped"

@dataclass(frozen=True)
class Order:
    id: str
    customer_id: str
    total: float
    status: OrderStatus

# Anti-Corruption Layer
class LegacyOrderGateway:
    def __init__(self, legacy_db):
        self._db = legacy_db

    async def get_order(self, order_id: str) -> Optional[Order]:
        # Legado usa tabela e campos com nomes históricos
        row = await self._db.fetchrow(
            "SELECT cod_pedido, cod_cliente, vlr_total, status_interno "
            "FROM tb_pedidos WHERE cod_pedido = $1",
            order_id
        )
        if not row:
            return None

        return Order(
            id=str(row["cod_pedido"]),
            customer_id=str(row["cod_cliente"]),
            total=float(row["vlr_total"]),
            status=self._translate_status(row["status_interno"]),
        )

    @staticmethod
    def _translate_status(legacy_status: str) -> OrderStatus:
        mapping = {
            "PE": OrderStatus.PENDING,
            "AP": OrderStatus.CONFIRMED,
            "EN": OrderStatus.SHIPPED,
        }
        if legacy_status not in mapping:
            raise ValueError(f"Status legado desconhecido: {legacy_status}")
        return mapping[legacy_status]

# Shadow mode — novo processa mas não responde ainda
class ShadowOrderService:
    def __init__(self, legacy, new_service, comparator):
        self._legacy = legacy
        self._new = new_service
        self._comparator = comparator

    async def get_order(self, order_id: str) -> Optional[Order]:
        legacy_result = await self._legacy.get_order(order_id)
        try:
            new_result = await self._new.get_order(order_id)
            # Compara silenciosamente — não afeta resposta
            await self._comparator.compare(order_id, legacy_result, new_result)
        except Exception as e:
            # Falha no novo sistema nunca afeta o usuário em shadow mode
            await self._comparator.record_error(order_id, e)
        return legacy_result  # sempre retorna legado durante shadow

Em shadow mode, falhas no novo sistema nunca afetam o usuário. A resposta vem sempre do legado — o comparador registra divergências silenciosamente.

Go — CatalogACL com tradução de vocabulário, OrderRouter com feature flag
// Novo modelo — limpo
package domain

type OrderStatus string

const (
    Pending   OrderStatus = "pending"
    Confirmed OrderStatus = "confirmed"
    Shipped   OrderStatus = "shipped"
)

type Order struct {
    ID         string
    CustomerID string
    Total      float64
    Status     OrderStatus
}

// Anti-Corruption Layer
package acl

type LegacyOrderGateway struct{ db *sql.DB }

func (g *LegacyOrderGateway) GetOrder(ctx context.Context, id string) (*domain.Order, error) {
    var (
        codPedido     string
        codCliente    string
        vlrTotal      float64
        statusInterno string
    )
    err := g.db.QueryRowContext(ctx,
        "SELECT cod_pedido, cod_cliente, vlr_total, status_interno FROM tb_pedidos WHERE cod_pedido = $1",
        id,
    ).Scan(&codPedido, &codCliente, &vlrTotal, &statusInterno)
    if errors.Is(err, sql.ErrNoRows) { return nil, nil }
    if err != nil { return nil, err }

    status, err := translateStatus(statusInterno)
    if err != nil { return nil, err }

    return &domain.Order{
        ID: codPedido, CustomerID: codCliente, Total: vlrTotal, Status: status,
    }, nil
}

func translateStatus(legacy string) (domain.OrderStatus, error) {
    switch legacy {
    case "PE": return domain.Pending, nil
    case "AP": return domain.Confirmed, nil
    case "EN": return domain.Shipped, nil
    default:   return "", fmt.Errorf("status legado desconhecido: %s", legacy)
    }
}

// Proxy de roteamento com feature flag
type OrderRouter struct {
    legacy  OrderGateway
    newSvc  OrderGateway
    flags   FeatureFlags
}

func (r *OrderRouter) GetOrder(ctx context.Context, id string) (*domain.Order, error) {
    if r.flags.IsEnabled("new-order-service") {
        return r.newSvc.GetOrder(ctx, id)
    }
    return r.legacy.GetOrder(ctx, id)
}

translateStatus isola o vocabulário do legado. OrderRouter usa feature flag para roteamento progressivo sem deploy — reversível a qualquer momento.

Quando a reescrita completa é a resposta honesta

O Strangler Fig não é sempre a resposta certa. Há situações onde uma reescrita completa é mais honesta e mais barata no longo prazo:

O legado não tem interfaces interceptáveis. Se o sistema legado não tem uma API, não tem ponto de entrada claro, e os chamadores acessam banco de dados diretamente — não há onde colocar o proxy de roteamento sem refatorar o legado antes. Às vezes o custo de criar interfaces no legado é equivalente ao custo de reescrevê-lo.

O domínio mudou radicalmente. Se as regras de negócio do sistema novo são fundamentalmente diferentes das do legado — não apenas tecnologia diferente, mas lógica de negócio diferente — o Strangler Fig carrega o risco de portar comportamentos errados do legado para o novo sistema. Uma reescrita a partir dos requisitos atuais pode produzir um sistema mais correto.

A dívida técnica é tão alta que cada mudança no legado custa 10×. Se manter o legado em paralelo enquanto o novo sistema é construído requer trabalho constante de manutenção e compatibilidade que consome toda a capacidade do time, o custo total pode ser maior que uma reescrita com o legado em modo de manutenção mínima.

A decisão honesta é medir o custo de cada caminho — incluindo o tempo de paralelismo, o custo de manter dois sistemas, o risco de não completar a migração. Nenhuma das abordagens é inerentemente superior: a certa depende do contexto.

Referências para aprofundar

  1. artigo StranglerFigApplication — Martin Fowler. martinfowler.com, 2004. O artigo original — a metáfora da figueira-estranguladora e a mecânica de migração progressiva.
  2. livro Monolith to Microservices — Sam Newman. O'Reilly, 2019. O livro mais completo sobre migração incremental — Strangler Fig, ACL, decomposição de banco.
  3. livro Domain-Driven Design — Eric Evans. Addison-Wesley, 2003. Cap 14: Anti-Corruption Layer — como proteger o novo modelo do vocabulário do legado.
  4. livro Working Effectively with Legacy Code — Michael Feathers. Prentice Hall, 2004. Técnicas práticas para trabalhar com código sem testes — seams, characterization tests e refatoração segura.
  5. artigo Things You Should Never Do, Part I — Joel Spolsky. joelonsoftware.com, 2000. O argumento clássico contra rewrites completas — por que big bang normalmente falha.
  6. artigo Feature Toggles (aka Feature Flags) — Pete Hodgson. martinfowler.com, 2017. Feature flags para roteamento progressivo — tipos, ciclo de vida e como evitar toggle debt.
  7. livro Designing Data-Intensive Applications — Martin Kleppmann. O'Reilly, 2017. Cap 11: Change Data Capture — como capturar mudanças no banco legado para replicar ao novo sistema.
  8. livro Continuous Delivery — Humble & Farley. Addison-Wesley, 2010. Deploy progressivo e feature flags — a base operacional para cortes seguros de sistema.
  9. artigo Parallel Change — Martin Fowler. martinfowler.com, 2014. Como fazer mudanças que permitem rollback — expand, migrate, contract em migrações de API.
  10. artigo The Strangler Application Pattern — Chris Richardson. microservices.io. Aplicação do padrão no contexto de extração de microservices de monolitos.
  11. livro Introducing Event Storming — Alberto Brandolini. Leanpub, 2021. Como descobrir fronteiras no legado — Event Storming como técnica de mapeamento do existente.
  12. livro Clean Architecture — Robert C. Martin. Prentice Hall, 2017. Cap 34: o limite entre sistemas e a importância do ACL para proteger o núcleo do domínio.