05 - Migração de Monolito para Microsserviços
video: https://youtu.be/... no frontmatter da nota.Migração de Monolito para Microsserviços
Este caso acompanha um ERP de logística construído como monolito Java de 1,2 milhão de linhas e 9 anos de idade, com 120 desenvolvedores em 8 times, deploy a cada 3 semanas em janela de 4 horas e rollback em 30% dos releases. As motivações são concretas e medidas: lead time de 45 dias entre commit e produção, acoplamento de escala em que o módulo de cotação força a escala do sistema inteiro, e conflito de merge crônico entre times que trava a entrega. Os objetivos arquiteturais são desacoplar releases e escala sem interromper a entrega de features, com proibição expressa de big bang. O desenho segue o padrão strangler fig: um roteador na frente, rotas migradas uma a uma e o monolito vivo durante toda a transição. A migração inteira tem orçamento e cronograma de 24 meses, com métricas de sucesso definidas antes da primeira extração.
Requisitos e pressupostos
As restrições do negócio definem o formato da migração:
- Zero downtime: clientes são operações de logística com uso contínuo, e nenhuma janela de indisponibilidade é aceitável.
- O monolito continua recebendo features por 2 anos: migração não é congelamento, e os times de produto não podem parar.
- Time de plataforma com 4 pessoas: qualquer proposta que exija plataforma grande é inviável.
- Comportamento equivalente verificado: nenhuma extração vai sem testes de paridade que comparem resposta antiga e nova com tráfego real.
- Orçamento sem big bang: a migração compete com roadmap de produto por capacidade de time.
- Rastreabilidade financeira durante a transição: nenhum pedido fica sem dono claro entre monolito e serviço novo em nenhum momento.
O dimensionamento do programa parte dos números do legado. O monolito atende 4 milhões de requisições por dia com picos de 800 por segundo na meia-noite, quando as cotações das transportadoras são recalculadas; o módulo de cotação responde por 35% dessas chamadas e escala em 20 vezes o próprio pico. Um time médio espera 45 dias para colocar uma mudança em produção, e 30% dos releases retornam por defeito, números que o programa usa como linha de base.
Os times foram reorganizados conforme as extrações, e não antes. Cada serviço extraído passa ao time que o usava no monolito, com dono único, e a plataforma serve os 8 times sem hierarquia central. O critério de sucesso organizacional é que nenhum time espere outro time para lançar, e o lead time por time aparece no mesmo painel das métricas técnicas.
Os pressupostos partem do diagnóstico honesto do legado. Os domínios já estão mapeados por uma iniciativa DDD anterior, o que poupou o projeto de refazer a modelagem. O banco compartilhado com 900 tabelas é o nó central do acoplamento, e nenhum plano que não enfrente os dados funciona. Os contratos internos do monolito são instáveis nos módulos jovens, então extração só faz sentido onde o contrato está estável ou pode ser estabilizado. A auditoria exige rastreabilidade completa durante a transição, com pedidos que atravessam o roteador identificáveis ponta a ponta.
Arquitetura proposta: strangler fig na prática
A peça central é um roteador na frente do monolito, com rotas migrando uma a uma sem mudança no cliente. O monolito permanece dono do banco durante a transição, e cada serviço extraído ganha banco próprio por caminho padronizado. Uma anti-corruption layer na fronteira traduz o modelo legado para o modelo de domínio novo, impedindo que decisões de 9 anos atrás contaminem o serviço novo. Para chamadas em sentido inverso, quando o monolito precisa do serviço novo, uma façade interna isola o ponto de integração. A configuração do roteador mostra a mecânica:
# Roteador na frente: rotas migradas saem do monolito sem mudar o cliente
upstream monolito { server monolito:8080; }
upstream cotacao { server cotacao:8080; }
upstream notificacoes { server notificacoes:8080; }
server {
# rotas extraídas: cada uma pode voltar ao monolito em um rollback de rota
location /api/cotacao/ { proxy_pass http://cotacao; }
location /api/notificacoes/ { proxy_pass http://notificacoes; }
# tudo que ainda não migrou segue para o monolito, sem exceção
location / { proxy_pass http://monolito; }
}
A sincronização de dados durante a transição é por captura de mudanças: enquanto um serviço novo usa tabelas ainda do monolito, o CDC espelha os dados para o banco próprio com verificação de paridade. A integração entre serviços extraídos é preferencialmente por eventos, publicados em Kafka, e chamadas síncronas são exceção com justificativa registrada. O roteador registra todas as rotas com versão, e o rollback de uma extração é voltar uma rota, operação de minutos que foi ensaiada em todas as fases.
A alternativa descartada foi a reescrita paralela, com sistema novo construído ao lado e cutover único. Dois projetos anteriores na mesma empresa falharam exatamente aí: o legado continuou crescendo durante a reescrita, e o alvo ficava para trás. O strangler fig aceita conviver com o legado em troca de entregas incrementais, e cada rota migrada vale por si só.
A observabilidade foi tratada como requisito de migração, e não de operação. Cada requisição atravessa o roteador com identificador de correlação, os serviços publicam traces no mesmo formato do monolito instrumentado, e o painel único mostra o percurso de um pedido cruzando monolito e serviços. Sem esse mapa, decidir o que extrair depois seria chute, porque os acoplamentos reais só aparecem nos traces de produção.
Os eventos seguem o vocabulário dos domínios extraídos: CotaCalculada, NotificacaoEnviada, ExpedicaoSeparada e FaturamentoFechado. O monolito publica os próprios fatos por outbox, e os serviços novos reagem sem chamada direta, o que reduz os pontos de sincronização durante a transição. Chamada síncrona entre monolito e serviço existe apenas na façada interna, para os casos em que o fluxo legado precisa de resposta imediata.
Os contratos entre monolito e serviços são versionados e gerados a partir do esquema, o que impede quebras silenciosas. Cada rota do roteador aponta uma versão, e a compatibilidade é verificada no pipeline antes do deploy, com teste de contrato contra o ambiente de produção em modo leitura. A façada interna concentra as chamadas em sentido inverso: quando o fluxo legado de faturamento precisa da cotação nova, chama a façada com timeout de 200 ms e fallback para o cálculo antigo em cache, e o volume diário dessas chamadas é métrica de desacoplamento, quanto menor, mais perto do fim da fase.
Fluxo principal: extração do primeiro serviço
A escolha do primeiro serviço define a reputação do programa inteiro. Os critérios aplicados foram quatro: baixo acoplamento de dados, alto valor para o negócio, dono claro entre os times e frequência de mudança que justifique o investimento. O primeiro extraído foi notificações, de baixo risco e baixo valor relativo, escolhido de propósito para ensinar o caminho com risco controlado; o segundo foi cotação, de alto valor, que resolveu o acoplamento de escala e provou o retorno. Nenhum serviço foi extraído por moda técnica: cada um entrou no cronograma por custo de coordenar mudanças no módulo legado.
O banco compartilhado é o nó do problema, e o caminho de separação segue quatro passos padrão. Primeiro, a lógica de acesso vira API interna do monolito, e o serviço novo nunca lê tabela direto. Segundo, o serviço novo consome essa API enquanto o comportamento estabiliza. Terceiro, o CDC espelha as tabelas relevantes para o banco próprio com conciliador diário de paridade. Quarto, o cutover de escrita acontece por roteador com reversão imediata disponível, e a verificação de paridade roda com tráfego sombra antes do corte.
O cutover de escrita é o momento mais tenso e o mais instrumentado. O conciliador compara linha a linha as tabelas espelhadas durante toda a fase de CDC, com divergência acumulada publicada no painel, e o corte só entra quando o contador está em zero por 3 dias consecutivos. Depois do corte, o monolito lê os dados pelo serviço novo pela façada, e as tabelas originais entram em modo leitura por 60 dias antes do desligamento.
Os números das primeiras extrações mostram o ritmo. Notificações levou 6 semanas do primeiro commit ao 100% do tráfego, com 2 semanas de sombra e nenhuma reversão; a equipe de 4 pessoas aprendeu o pipeline, a ACL e o CDC nesse módulo. Cotação levou 14 semanas, com 3 reversões de rota durante a paridade, porque o módulo guardava 9 anos de regras acumuladas em 40 tabelas; o banco próprio ficou com 12 tabelas, e o resto morreu por falta de uso.
# Anti-corruption layer: o modelo legado não vaza para o serviço novo
class CotacaoACL:
def para_dominio(self, dto_legado):
# legado guarda preço em centavos com imposto embutido; o domínio separa
return Cotacao(
id=IdLegado.para_uuid(dto_legado["codCot"]),
valor_bruto=Centavos(dto_legado["vlrTot"]) - Imposto.extrair(dto_legado),
# formato de data próprio de 1998, mantido longe do domínio novo
validade=Converter.legado_para_iso(dto_legado["dtVal"]),
)
def para_legado(self, cotacao):
# a saída reconstrói o contrato que o monolito entende, sem exceção
return {"codCot": IdLegado.para_codigo(cotacao.id),
"vlrTot": cotacao.valor_total_com_imposto().centavos,
"dtVal": Converter.iso_para_legado(cotacao.validade)}
A ACL é escrita com testes de tradução exaustivos, porque ela é o único lugar onde os dois modelos se tocam. Cada campo do contrato legado tem dono no domínio novo, e campos sem tradução clara são sinal de que o corte de domínio está errado, e não de que a ACL precisa de exceção.
Decisões e trade-offs
| Fase | Duração | Escopo | Critério de saída |
|---|---|---|---|
| 0. Fundações | 3 meses | Plataforma, roteador, observabilidade e CI/CD por serviço | Deploy diário sem janela em ambiente de produção |
| 1. Primeiras extrações | 4 meses | Notificações e cotação com banco próprio | 5% do tráfego em serviços, paridade verificada |
| 2. Aceleração | 8 meses | Expedição, rastreamento e faturamento | 40% do tráfego, lead time abaixo de 20 dias |
| 3. Fronteira de dados | 5 meses | Últimas tabelas compartilhadas e desligamento de acessos | Nenhum acesso direto entre serviços e banco alheio |
| 4. Consolidação | 4 meses | Desativação de módulos do monolito e revisão de custo | Monolito reduzido a 40% das linhas originais |
A leitura da tabela de fases mostra o desenho do risco. As duas primeiras fases gastam 7 meses em fundações e exemplos, e é o período que a pressão do negócio costuma atacar; cortá-lo repete o erro dos projetos de reescrita que o programa descartou. As fases centrais acumulam valor visível mês a mês, e a última fase é manutenção, e não construção.
As métricas de sucesso foram definidas antes da primeira extração e medidas por fase. O lead time caiu de 45 para 12 dias na fase 2, e a frequência de deploy subiu de 1 por mês para 40 por mês somando os serviços, com o monolito mantendo seu ritmo próprio. O MTTR caiu de 4 horas para 25 minutos, porque o rollback por roteador elimina a necessidade de reimplantar o sistema inteiro. O número de serviços nunca foi métrica: a régua é o fluxo de valor, e o programa seria bem-sucedido mesmo com menos extrações se as métricas atingissem a meta.
| Métrica | Antes | Meta na fase 2 | Medido no fim |
|---|---|---|---|
| Lead time de feature | 45 dias | 20 dias | 12 dias |
| Frequência de deploy | 1 por mês | 20 por mês | 40 por mês |
| MTTR | 4 horas | 1 hora | 25 minutos |
| Tráfego em serviços novos | 0 | 40% | 55% |
| Rollback de release | 30% | 10% | 6% |
O cronograma de 24 meses foi defendido com duas simulações. Comprimir para 12 meses exigiria dobrar o time de plataforma e congelar features, custo rejeitado pelo negócio; esticar para 36 meses desaceleraria na metade, quando a motivação inicial esfria. O formato por fases com critério de saída permite pausar sem perder o caminho, porque cada fase termina com o sistema inteiro operável.
O custo de rodar em paralelo é o preço aceito do strangler fig. Durante a fase 3, os mesmos dados vivem em dois bancos com CDC e conciliador, e o custo de infraestrutura subiu 18% no pico da sobreposição, folga reservada no orçamento desde o início. O retorno veio no mesmo período, porque a extração da cotação derrubou o custo de escala do monolito em 25%, e a curva cruzou antes do fim do programa.
Escalabilidade e falhas: riscos e armadilhas
A armadilha número um é o monolito distribuído: extrair módulos mantendo chamadas síncronas em cadeia recria o acoplamento com latência pior e falha parcial nova. A mitigação foi regra explícita: profundidade máxima de duas chamadas síncronas entre serviços, integração preferencial por eventos, e revisão arquitetural obrigatória quando a regra precisa de exceção. O segundo risco é o banco compartilhado remanescente, que vira dependência eterna quando um serviço novo lê direto as tabelas do legado; a regra é acesso só por API do dono ou por CDC, e o conciliador aponta violação.
Outras armadilhas merecem registro. A paridade obsessiva, extrair módulo replicando 100% dos recursos, incluindo os abandonados, foi evitada por inventário de uso antes de cada extração, e 15% das features candidatas foram descartadas por falta de uso. Times sem plataforma produziriam 8 formas diferentes de fazer deploy, e o golden path da plataforma desde a fase 0 impediu o custo composto. A escala heterogênea, um dos motivos da migração, foi verificada na prática: a cotação escala 20 vezes no pico da meia-noite, e pela primeira vez a escala dele não arrasta o faturamento junto.
As falhas da migração têm protocolo. Toda extração entra com sombra de tráfego por 2 semanas, comparação de resposta e reversão de rota ensaiada; no programa inteiro, 3 extrações foram revertidas e refeitas, e nenhuma virou incidente de cliente. O conciliador diário de dados roda durante toda a fase 3, e qualquer divergência acima de zero trava o cutover daquele domínio até a causa ser eliminada.
Um incidente ilustra o valor das rotas reversíveis. Na fase 2, a extração do faturamento introduziu divergência de arredondamento percebida por um cliente grande; a rota voltou ao monolito em 4 minutos, o corretivo levou 3 dias, e a segunda entrada passou com sombra estendida para 4 semanas. O episódio entrou no registro do programa como argumento pelo critério de saída rígido de paridade.
O golden path da plataforma encurta o caminho de cada extração. Template de serviço com pipeline, observabilidade, segredos e padronização de segurança prontos reduz a criação de um serviço novo de 3 semanas para 2 dias, e o time de 4 pessoas sustenta os 8 times com esse molde. O custo é disciplina: desvios do molde precisam de justificativa, e a plataforma é dona do template, não dos serviços.
Cada serviço extraído herda deploy canário pelo pipeline da plataforma: 5% do tráfego por 30 minutos com comparação automática de taxas de erro e latência, seguido de rampa até 100%. O monolito mantém seu deploy tradicional durante a transição, e as duas cadências convivem no mesmo roteador sem conflito, porque as rotas isolam o destino.
O custo é monitorado por serviço desde a fase 1, com tag de alocação por domínio, e a revisão trimestral compara o custo por rota migrada contra o custo estimado no monolito. O desvio mais comum é o over-provisioning defensivo dos serviços novos, corrigido pelo uso real após 60 dias de métricas.
Lições aplicáveis
- Extraia primeiro o que ensina, não o que impressiona: notificações, de baixo risco, ensinou o caminho completo que depois carregou cotação, de alto valor.
- O código se separa em semanas, os dados em meses: o banco compartilhado é o último laço a romper, e o cronograma precisa refletir essa assimetria.
- A anti-corruption layer é seguro, e não luxo: cada exceção permitida ao modelo legado no serviço novo virou dívida documentada e cara.
- Meça a migração pelo fluxo de valor: lead time, frequência de deploy e MTTR são a régua; número de serviços extraídos é vaidade.
- Monolito distribuído é a armadilha dominante: chamada síncrona em cadeia recria o acoplamento com pior latência, e a regra de profundidade precisa ser fiscalizada.
- Mantenha o monolito vivo: congelar o legado transforma migração em guerra com o produto, e a entrega de features no monolito continuou até o fim.
- Inventário de uso antes de extrair: 15% das features candidatas morreram por falta de uso, e o módulo novo nasceu menor.
- Observabilidade antecede extração: os traces de produção apontaram os acoplamentos reais que o diagrama da arquitetura não mostrava.
- A rota reversível vale o esforço: rollback de extração em minutos pelo roteador deu coragem para acelerar as fases seguintes.