Uma plataforma de vendas pela qual sou responsável tecnicamente está em operação desde 2012. Ela conecta aplicações Android e Windows, serviços Java, ERP, PostgreSQL e bases SQLite locais. Atende mais de 60 empresas e cerca de 700 dispositivos, parte deles sujeita a conexão intermitente e versões diferentes do cliente.
Esse contexto muda a pergunta. A equipe não escolhe entre um legado parado e uma arquitetura ideal. Escolhe como alterar uma operação viva sem obrigar todos os usuários a atualizar juntos, sem perder dados offline e sem transformar uma publicação em uma aposta difícil de desfazer.
Quando coexistência e rollout gradual são viáveis, prefiro modernizar por fatias verticais. Não é uma lei nem a opção superior para todo sistema. A abordagem limita cada aposta: preserva o comportamento relevante, cria uma fronteira substituível e muda uma parte antes de avançar. A arquitetura transitória custa tempo e código, mas produz evidência durante o trabalho.
O legado não é uma idade
Um sistema de 2012 não contém treze anos do mesmo risco. Há código antigo que quase não muda e há componentes recentes que concentram falhas, conhecimento e dependências. Colocar tudo sob o rótulo de "legado" esconde justamente as diferenças que deveriam orientar o investimento.
Separar idade, risco e valor
Eu avalio três dimensões sem reduzi-las a uma nota. A primeira é obsolescência: JVM, servidor de aplicação, bibliotecas, build e disponibilidade de patches. Java 8, por exemplo, só pode ser aceitável se a distribuição e o build usados ainda estiverem cobertos por suporte e atualizações compatíveis com a política da organização.
A segunda dimensão é risco operacional. Entram criticidade, exposição, recuperação, qualidade dos dados, ausência de testes, acoplamento e concentração de conhecimento. A terceira é valor: receita ou operação protegida, mudança de produto bloqueada, prazo regulatório e custo que pode ser evitado.
A AWS propõe avaliar adequação estratégica, funcional, técnica e financeira antes de agrupar e sequenciar aplicações.[1] O modelo ajuda, mas a decisão continua local. Um batch tecnicamente ruim, com desligamento confirmado para o trimestre seguinte, pode pedir contenção. Já uma rotina estável que bloqueia toda alteração comercial pode merecer a primeira fatia, embora quase nunca gere incidente.
No ecossistema de vendas, a longevidade não justificava uma substituição completa. Regras consolidadas, clientes em ritmos diferentes e dados mantidos nos dispositivos tornavam a paridade difícil de provar em um grande corte. O objetivo passou a ser menos vistoso e mais útil: diminuir o risco e o tempo da próxima mudança relevante.
Descobrir o sistema que existe
O primeiro artefato de modernização não deveria ser o desenho da arquitetura futura. Antes dele, preciso saber o que realmente é construído, executado, chamado e persistido. Wiki e entrevistas ajudam, mas não bastam. O inventário deve ser confrontado com repositórios, pipelines, manifests, catálogos de banco, telemetria e tráfego observado.
Inventário com limites explícitos
No mundo Java, mvn dependency:tree ou o equivalente do Gradle mostra o grafo resolvido pelo build, não o inventário inteiro. Pode deixar de fora componentes carregados de forma dinâmica, bibliotecas do runtime, código copiado e artefatos externos. Uma SBOM amplia a visibilidade, mas depende do escopo e da qualidade da ferramenta.
O OWASP Dependency-Check relaciona dependências a vulnerabilidades publicadas e tem integrações com Maven e Gradle.[12] O resultado precisa de triagem: uma associação automática não demonstra que o caminho vulnerável é alcançável, assim como a ausência de alerta não prova segurança.
Dados e integrações exigem o mesmo cuidado. No caso que conduz este artigo, PostgreSQL central, SQLite local, ERP, APIs e sincronização formam uma unidade operacional. Inventariar apenas os módulos Java produziria uma imagem limpa e falsa. Também é preciso registrar versões ativas, consumidores externos, jobs, procedimentos manuais, volumes, SLOs e responsáveis.
Proteger o comportamento antes de movê-lo
Michael Feathers descreve testes de caracterização como uma forma de colocar código legado sob controle antes da alteração.[2] Eles registram o comportamento existente, inclusive peculiaridades que talvez ninguém desenhasse hoje. Não afirmam que o resultado é correto; tornam a diferença visível.
Começo pelas rotas da fatia escolhida. Capturo entradas e saídas, regras de cálculo, efeitos no banco e mensagens publicadas. Se houver um defeito conhecido, separo o teste do estado atual daquele que expressa a correção aprovada. Cobertura indica buracos; não prova que os contratos importantes foram protegidos.
Feathers chama de seam o ponto em que o comportamento pode ser alterado sem editar o código naquele ponto.[3] Interfaces, adaptadores, injeção de dependência e portas HTTP são exemplos que uso em Java; não são uma lista normativa do autor. A fronteira deve cobrir a necessidade concreta. Uma abstração que reproduz toda a API de um fornecedor só troca um acoplamento por outro com nome interno.
A localização da fronteira escolhe o mecanismo
Strangler Fig e Branch by Abstraction costumam aparecer como alternativas concorrentes. A distinção que mais me ajuda é física: a substituição acontece entre aplicações ou dentro do mesmo processo?
Entre aplicações: Strangler Fig
Quando uma rota, operação ou evento pode ser desviado na borda, a abordagem Strangler Fig permite colocar a implementação nova ao redor da antiga e retirar o caminho legado aos poucos. Fowler a apresenta como uma abordagem de modernização incremental, com atenção à decomposição, à entrega e ao custo da arquitetura transitória.[4] Ela não oferece garantias universais; o valor depende de uma fronteira que possa ser observada e controlada.
Na plataforma de vendas, uma fatia poderia ser uma operação completa de consulta ou sincronização, não uma separação artificial entre controller, service e repository. Uma fachada pode manter o contrato usado por clientes de versões diferentes, encaminhar uma coorte limitada ao caminho novo e comparar resultados. O sistema antigo permanece como referência enquanto a equipe ainda não provou compatibilidade e reconciliação.
A unidade do desvio precisa fazer sentido para a operação: versão do cliente, empresa, capacidade ou percentual de tráfego. Cortar uma transação fortemente consistente apenas para criar um serviço novo aumenta o risco.
Dentro do processo: Branch by Abstraction
Se a troca ocorre dentro da aplicação, como substituir um cliente de integração, ORM ou mecanismo de regras, Branch by Abstraction tende a oferecer uma fronteira melhor. A sequência descrita por Fowler é introduzir uma abstração, migrar consumidores para ela, implementar o substituto e retirar o caminho anterior.[5] A alternância gradual de consumidores só entra quando a arquitetura permite.
Esse mecanismo mantém o código liberável sem sustentar um branch longo, mas cria dívida transitória. Flags e adaptadores precisam de dono, telemetria e condição de remoção. Sem isso, cada migração deixa mais uma bifurcação permanente.
Nos dois mecanismos, prefiro mudança aditiva antes da destrutiva. Parallel Change formaliza essa cadência: expandir o contrato, migrar consumidores e só então contrair a superfície antiga.[6] Isso pode significar aceitar dois formatos por uma janela, adicionar um método antes de remover outro ou publicar uma versão de evento enquanto a anterior ainda circula. Compatibilidade temporária tem custo, mas evita exigir que centenas de dispositivos e o servidor mudem no mesmo instante.
Dados e operação definem a reversibilidade
O binário costuma ser mais simples de reverter do que uma transformação de dados. Nem isso acontece necessariamente em minutos: o tempo depende do mecanismo de implantação, das dependências disponíveis e dos efeitos externos já produzidos. Tratar rollback como "usar a versão anterior" ignora a parte mais difícil.
Expandir, migrar e contrair os dados
Sadalage e Fowler defendem mudanças pequenas de banco, versionadas e atravessando o pipeline da aplicação.[7] Na prática, começo adicionando estruturas sem remover as antigas. Implanto código que entende os dois modelos, faço o backfill em lotes reiniciáveis e comparo invariantes de negócio. Só depois mudo leituras e encerro a escrita antiga. A remoção vem após uma janela que cubra clientes offline, filas e procedimentos de recuperação.
Escrita dupla, outbox e CDC não são peças intercambiáveis. Escrita dupla pode produzir estados parciais quando envolve transações independentes. Outbox aproxima a publicação da transação da fonte. CDC observa mudanças no log do banco. A escolha depende de atomicidade, ordenação, latência, replay e de qual sistema é a fonte de verdade em cada fase. O nome do mecanismo importa menos que as garantias que a operação exige.
Idempotência também depende do efeito. Mensagens sujeitas a nova entrega precisam de identificador estável quando a repetição pode causar dano; o consumidor deve tornar o efeito idempotente ou deduplicá-lo nesse contexto. Recalcular uma projeção é diferente de emitir uma cobrança. Repetir indefinidamente uma operação que falha não é recuperação.
Antes do rollout, deixo explícito o que a reversão faz com dados já produzidos. Pode significar redirecionar leituras, reprocessar um log, restaurar um snapshot ou aplicar uma migração compensatória. Transformações destrutivas não deveriam compartilhar a mesma janela que ativa o caminho novo.
Observar antes de desviar tráfego
OpenTelemetry documenta o agente Java como ponto de partida para instrumentação automática e a correlação de traces, métricas e logs por contexto.[8] Isso revela chamadas HTTP, JDBC e mensageria, mas não substitui métricas do domínio. Em vendas, latência saudável não compensa divergência de preço, item não sincronizado ou pedido duplicado.
JDK Flight Recorder, integrado ao HotSpot, costuma operar com baixo overhead quando configurado de forma adequada.[9] O custo varia conforme eventos, frequência, stack traces e carga; por isso, deve ser medido no ambiente real. Heap, GC, threads e pools ajudam a explicar regressões, enquanto os indicadores funcionais dizem se a fatia preservou o trabalho do usuário.
Deploy e rollout são decisões diferentes. O artefato pode entrar desativado, ser habilitado para uma coorte pequena e avançar somente após uma janela de observação. O Google SRE define canário como uma implantação parcial, limitada no tempo e comparada com um controle.[10] Os critérios de promoção e interrupção precisam existir antes do primeiro desvio. Uma flag ainda não exercitada na direção de volta é apenas uma intenção de rollback.
Modernizar até poder remover
Contar serviços, linhas migradas ou versões atualizadas mede atividade. Para saber se a modernização funcionou, observo resultado operacional, confiabilidade e fluxo de entrega. A DORA organiza cinco métricas entre throughput e instabilidade.[11] Elas ajudam a acompanhar a capacidade de entrega, mas não substituem medidas locais como divergência de dados, tempo de sincronização, idade de flags e tempo necessário para diagnosticar uma falha.
Reescrita é uma heurística, não uma derrota
Evitar um grande corte também não deve virar dogma. Considero a reescrita uma candidata quando o sistema é pequeno e compreendido, o escopo pode permanecer estável, há testes de aceitação fortes, dados e integrações são simples, e existe uma data crível para desligar o legado. Software commodity que pode ser substituído e produtos sem usuários ou dados relevantes também mudam a conta.
Esses são critérios heurísticos deste artigo, não limiares validados. Em um domínio grande, ativo e cheio de regras tácitas, a reescrita tende a adiar a descoberta das diferenças para o momento mais caro: a migração. No ecossistema em operação desde 2012, preservar compatibilidade e aprender por fatias foi mais coerente com as restrições do que tentar reproduzir de uma vez tudo o que anos de uso haviam consolidado.
Seis perguntas antes da primeira fatia
- Qual resultado de negócio ou SLO deve melhorar, e qual é a linha de base?
- Que fatia vertical entrega evidência útil com dependências controláveis?
- A fronteira está entre aplicações ou dentro do processo?
- Qual é a fonte de verdade dos dados em cada fase, e como as divergências serão reconciliadas?
- Quais sinais interrompem o rollout, e o que a reversão faz com efeitos já produzidos?
- Que evidência autoriza remover rota, flag, adaptador, schema ou implementação antiga?
A entrada de uma tecnologia nova em produção não encerra a modernização. A fatia termina quando o caminho antigo pode ser removido com evidência, ou quando há uma decisão explícita e fundamentada para mantê-lo. Até lá, houve implantação. A redução de legado ainda está em curso.
Referências
- Evaluating modernization readiness for applications in the AWS Cloud
- Working Effectively with Legacy Code — Michael Feathers
- Seams — Michael Feathers
- Strangler Fig Application — Martin Fowler
- Branch By Abstraction — Martin Fowler
- Parallel Change — Danilo Sato
- Evolutionary Database Design — Pramod Sadalage e Martin Fowler
- OpenTelemetry Java Instrumentation
- JDK Flight Recorder — Dev.java
- Canarying Releases — Google SRE Workbook
- DORA software delivery performance metrics
- OWASP Dependency-Check