Seu monólito tem dez anos, cada release exige um longo ciclo de regressão e alguém propõe uma reescrita: construir o sistema novo ao lado, atingir a paridade de funcionalidades e trocar num fim de semana. Enquanto isso, o sistema antigo continua mudando e a data da troca escorrega. Martin Fowler escreve que ele e seus colegas viram esse plano «ir por água abaixo na maioria das vezes».
O padrão strangler fig substitui o sistema peça por peça. Uma fachada na frente do monólito roteia cada requisição para o código antigo ou para um serviço novo, e as capacidades migram uma a uma até que nada seja roteado para o monólito. O roteamento é a parte fácil. Propriedade dos dados, prova de paridade e terminar o trabalho são as difíceis.
Este artigo cobre tudo isso, com um diagrama de uma migração em andamento.
De onde vem o nome
Em 2001, Fowler viu figueiras-mata-pau na floresta tropical de Queensland: trepadeiras que germinam sobre uma árvore hospedeira, criam raízes no solo e podem sobreviver à árvore, deixando a figueira com a forma dela. Em junho de 2004 ele as usou como metáfora para substituir um sistema legado aos poucos. Em abril de 2019 renomeou o artigo de «Strangler Application» para «Strangler Fig Application», para remeter à planta, e o reescreveu em agosto de 2024. A versão atual enfatiza o que as equipes resistem em aceitar: a arquitetura de transição, código que você escreve só para que o velho e o novo convivam.
A fachada faz o roteamento
A fachada é um reverse proxy ou API gateway na frente do monólito: NGINX, Envoy, um API gateway de nuvem ou um load balancer com regras de caminho. Ela começa enviando tudo ao monólito. Depois disso, cada passo da migração é uma mudança de roteamento. Roteie por:
- Caminho:
/catalog/*vai para o serviço de catálogo. - Método:
GET /orders/*antes dePOST /orders, para que as leituras migrem primeiro. - Percentual: um hash do ID de sessão ou de usuário mantém cada usuário de um só lado.
- Header ou tenant: usuários internos ou um cliente piloto vão primeiro.
# upstream blocks catalog, orders and monolith defined elsewhere
split_clients "${cookie_session}" $orders_read {
25% orders; # canary share for reads
* monolith;
}
map $request_method $orders_target {
GET $orders_read;
default monolith; # order writes stay in the monolith
}
server {
location /catalog/ { proxy_pass http://catalog; }
location /orders/ { proxy_pass http://$orders_target; }
location / { proxy_pass http://monolith; }
}
O rollback é uma única edição: aponte a linha de 25% para monolith (o nginx rejeita uma entrada de 0%). Aplique o hash a uma chave que toda requisição carregue: requisições sem o cookie caem todas no mesmo bucket.
Mantenha a fachada enxuta: roteamento, TLS, autenticação e logging, sem regras de negócio. Ela fica em toda requisição, então rode várias instâncias e meça a latência que ela adiciona. O diagrama de arquitetura de API gateway mostra o que pertence a um gateway.
As quatro fases de uma migração
| Fase | A fachada roteia | Dados | Concluída quando |
|---|---|---|---|
| 0. Fachada primeiro | Tudo para o monólito | Inalterados | Fachada no ar, latência adicionada medida |
| 1. Primeira capacidade | Uma rota, como /catalog/*, para um serviço novo |
O serviço lê as tabelas do monólito ou uma cópia CDC, depois as próprias | Rota a 100% durante um ciclo de negócio completo |
| 2. Maior parte do tráfego em serviços novos | A maioria das rotas; o monólito mantém uma lista conhecida | Cada serviço é dono das suas tabelas | O que sobrou cabe em uma página |
| 3. Monólito aposentado | Nada para o monólito | Banco legado arquivado e depois apagado | Monólito desligado |
A página do padrão Strangler Fig da Microsoft acrescenta um passo final: remover a fachada, ou mantê-la como adaptador para clientes legados.
Escolhendo o que extrair primeiro
Bons primeiros candidatos têm três traços:
- Uma costura clara, nas palavras de Michael Feathers um lugar onde você pode alterar o comportamento sem editar naquele lugar: um prefixo de URL, uma fila, o limite de um módulo.
- Uma taxa de mudança alta, para que a migração compense logo. O controle de versão mostra isso.
- Baixo acoplamento: poucos joins com outras tabelas, poucas chamadas de volta ao monólito.
git log --since="12 months ago" --format= --name-only -- src/ \
| sort | uniq -c | sort -rn | head -20
Um ranking para uma loja online:
| Candidato | Costura | Taxa de mudança | Acoplamento | Veredito |
|---|---|---|---|---|
| Navegação do catálogo | /catalog/* |
Alta | Baixo | Primeiro |
| Histórico de pedidos | GET /orders/* |
Média | Médio | Segundo, leituras primeiro |
| Checkout | Chama preços, estoque e pagamentos | Alta | Alto | Depois das suas dependências |
| Relatórios noturnos | SQL sobre todo o esquema | Baixa | Muito alto | Por último, ou um feed para um data warehouse |
Extraia por capacidade de negócio, não por camada técnica. Um «serviço de acesso a dados» compartilhado não move nada e acopla tudo.
Movendo os dados
O roteamento move requisições, não dados, e é nos dados que as migrações travam. Decida quem é dono de cada tabela em cada passo.
| Estratégia | Como funciona | Use para | Risco |
|---|---|---|---|
| Banco compartilhado | O serviço novo usa as tabelas do monólito | As primeiras semanas | Mudanças de esquema do monólito o quebram |
| Change data capture | Um conector transmite as mudanças de linhas a partir do log do monólito | Mover as leituras primeiro | Atraso, esquemas legados, fluxo em uma só direção |
| Banco por serviço | O serviço é dono das suas tabelas; os outros chamam sua API | O estado final | Joins entre serviços viram chamadas de API |
| Dupla escrita a partir do código | A aplicação escreve nos dois bancos | Nada | Uma falha entre as escritas deixa os bancos dessincronizados |
A orientação da Microsoft dá a ordem usual: o serviço novo começa nas tabelas do monólito, depois ganha o próprio banco, carregado uma vez e mantido atualizado por CDC, e vira o sistema de registro após a validação. Enquanto as tabelas legadas existirem, o rollback é uma mudança de rota; depois, é uma restauração.
Evite a dupla escrita: uma falha entre duas escritas separadas deixa os bancos em desacordo, e atualizações concorrentes podem chegar em ordens diferentes. Faça de uma única transação a origem de cada mudança, via CDC ou um transactional outbox. O diagrama de change data capture cobre o Debezium e os replication slots.
A camada anticorrupção
O modelo do monólito vaza: códigos de status de uma letra, campos de cobrança e entrega na mesma tabela, regras em stored procedures. Um serviço que consome essas linhas herda o modelo que você está deixando. Uma camada anticorrupção, um padrão do Domain-Driven Design de Eric Evans (2003), traduz na fronteira:
- Legado para novo: eventos CDC com linhas legadas viram pedidos no modelo novo.
- Novo para legado: o serviço de pedidos lê dados de clientes do monólito por meio de um adaptador.
- Monólito para novo: depois que o catálogo migra, o código do monólito chama a API do catálogo em vez de suas tabelas antigas.
Mantenha regras de negócio fora da camada. Em um serviço hexagonal, ela vive nos adaptadores, de modo que o núcleo nunca vê tipos legados.
Provando a paridade antes de mover uma rota
Toda rota passa pelos mesmos passos:
- Shadow. A fachada copia as requisições para o serviço novo e descarta as respostas dele, como fazem o módulo mirror do NGINX e o request mirroring do Envoy. Espelhe só leituras: um
POST /ordersespelhado cria um segundo pedido. - Comparar. Compare as duas respostas depois de normalizar timestamps, IDs e ordenação. O Diffy do Twitter, publicado como código aberto em 2015, roda duas cópias do código antigo e ignora como ruído o que difere entre elas. Dentro do monólito, a biblioteca Scientist do GitHub (Ruby, com ports para outras linguagens) executa lado a lado o caminho antigo e o novo e devolve o resultado do antigo.
- Canary. Mova 1%, 5%, 25%, 50% e depois 100% do tráfego real, compare a taxa de erros e a latência p99 com as do monólito para a mesma rota e mantenha cada passo durante um pico diário. O diagrama de estratégias de deploy cobre a mecânica.
- Manter. Em 100%, mantenha o caminho antigo implantável e seus dados atualizados durante eventos como um fechamento de fim de mês.
- Apagar o código antigo, suas tabelas e a rota.
Devolver uma rota só funciona enquanto o monólito tiver dados atuais. Quando o serviço novo for dono das escritas, devolva-as por CDC reverso ou eventos, ou um rollback as perderá.
Como as migrações strangler fig falham
- Nunca terminar. Os 80% fáceis migram; relatórios, jobs em lote e telas de administração ficam, e você opera dois sistemas. Conte o que resta a cada sprint e defina uma data de desligamento.
- Um monólito distribuído. Serviços que compartilham tabelas, fazem deploy juntos ou se chamam em longas cadeias síncronas. O teste é um escritor por tabela.
- A fachada como gargalo: lógica de negócio no gateway, ou uma única instância carregando todo o tráfego.
- Consumidores ocultos. Relatórios e outras equipes leem direto o banco do monólito, sem que a fachada os veja. Confira os logs de conexão antes de mover uma tabela.
- Paridade de funcionalidades como meta. Fowler observa que muito do comportamento legado «não é realmente desejado». Patterns of Legacy Displacement cobre essa e outras armadilhas.
O diagrama da migração strangler fig
Conteúdo do diagrama
- Customers
- Facade (API gateway)
- Catalog service
- Orders service
- Legacy monolith
- Catalog DB
- Orders DB
- Monolith DB
- Debezium + Kafka
- Anti-corruption layer
- HTTPS
- /catalog/* · 100%
- GET /orders/* · 25%
- everything else + 75% of GET /orders
- reads, writes
- reads
- WAL changes
- order row events
- upserts in the new model
Como ler:
- Da esquerda para a direita: os clientes chamam a fachada, que divide o tráfego por rota; os bancos de dados ficam à direita.
- Verde, migrado: todo o
/catalog/*vai para o serviço de catálogo, que é dono dos seus dados. - Âmbar, canary: 25% de
GET /orders/*vai para o serviço de pedidos. As escritas e as outras leituras ainda vão para o monólito. - Cinza, legado: o monólito atende o resto e ainda é dono dos dados de pedidos.
- Roxo, tracejado: o Debezium transmite as mudanças de pedidos do WAL do monólito pelo Kafka, e a camada anticorrupção as mapeia para o modelo novo.
- A tabela de rotas lista destino, percentual e fase por rota, de modo que o rollback é uma única edição.
Desenhando migrações strangler fig com clareza
- Um diagrama por fase, ou um título com data; guarde os antigos para mostrar a direção.
- Rotule as rotas com caminho e percentual, como «GET /orders/* · 25%».
- Mostre a propriedade dos dados: o sistema de registro e a direção de cada sincronização.
- Colora por status (migrado, canary, legado), não por equipe.
- Deixe de fora os detalhes internos. Isto é um mapa de migração, não um diagrama de componentes.
- Desenhe os consumidores ocultos, como um relatório noturno que lê o banco do monólito.
O diagrama de arquitetura de microsserviços mostra como manter legível o estado final.
Monte no Archboard
- Entre com Google ou GitHub e crie um diagrama.
- Adicione uma Pessoa do pack C4 Model para os clientes e um Gateway de API do pack Nuvem para a fachada.
- Adicione três nós Contêiner do pack C4 Model: Catalog service, Orders service e Legacy monolith. Pinte-os de verde, âmbar e cinza.
- Adicione um nó Postgres do pack Dados à direita de cada contêiner.
- Entre o banco do monólito e o banco de pedidos, empilhe um nó Kafka do pack Dados (Debezium + Kafka) e um Componente do pack C4 Model (Anti-corruption layer), conectados para cima com arestas tracejadas.
- Desenhe as rotas a partir da fachada, rotuladas com caminho e percentual, e adicione acima de cada contêiner um rótulo de status Texto do pack Básico.
- Adicione um bloco Markdown do pack Básico com a tabela de rotas.
Se o seu mapa de rotas vive em um flowchart Mermaid, o conversor de Mermaid o transforma em um primeiro rascunho editável.
O exemplo usa 11 nós de diagrama e fica dentro do plano Free.
Perguntas frequentes
O que é o padrão strangler fig? Uma forma de substituir um sistema legado aos poucos: uma fachada roteia cada requisição para o código antigo ou para um serviço novo, as capacidades migram uma a uma e o sistema antigo é desligado quando nada é roteado para ele.
Por que se chama padrão strangler fig? Martin Fowler o batizou em 2004 por causa das figueiras-mata-pau que viu em Queensland, que crescem ao redor de uma árvore hospedeira e podem substituí-la. Ele acrescentou Fig ao nome em 2019 para destacar a planta.
Como lidar com o banco de dados em uma migração strangler fig? Em etapas. O serviço novo primeiro lê as tabelas do monólito, depois ganha o próprio banco alimentado por change data capture e assume as escritas após a validação. Evite a dupla escrita a partir do código da aplicação.
Qual é a diferença entre strangler fig e branch by abstraction? O strangler fig roteia requisições na borda do sistema. O branch by abstraction atua dentro do código: uma interface permite que a implementação antiga e a nova convivam até que todo chamador tenha migrado. Muitas migrações usam os dois.
Quando não usar o padrão strangler fig? Quando as requisições ao sistema antigo não podem ser interceptadas, quando o sistema é pequeno o bastante para ser substituído de uma vez ou quando precisa ser desligado rápido.