A Alura, como qualquer empresa de tecnologia, também tem legado pra manter. Até pouco tempo atrás, nossa aplicação ainda rodava Spring Boot 2.7, cujo suporte open source terminou em novembro de 2023.
Fazer uso de um framework sem suporte é como uma bomba-relógio: uma hora ela vai explodir, seja do ponto de vista de segurança, seja do ponto de vista de evolução do sistema.
Nós sabíamos há muito tempo que a atualização era necessária. O problema é que uma migração desse tamanho custa tempo, disputa espaço com outras entregas e é difícil de priorizar enquanto o sistema continua funcionando.
E não era uma atualização pequena. Estamos falando de uma aplicação usada diariamente por milhares de pessoas, com pagamentos, integrações externas e várias formas de login. Muita coisa pode quebrar.
Já tínhamos tentado fazer essa migração de maneira manual em 2024/2025, no mundo pré-IA. Na época, decidimos dividir o trabalho entre alguns dos membros mais seniores. A iniciativa não decolou porque acabamos travando em migraçõs de coisas muito específicas como SAML.
Com o time da Alura cada vez mais familiarizado com engenharia de software agêntica, nos sentimos preparados para o desafio e resolvemos tentar mais uma vez, aproveitando o lançamento recente do Astra, o momento mais novo da OpenAI.
Primeiro passo: estruturando o prompt da migração e disparando agentes
Começamos com um prompt bem detalhado sobre como o agente deveria trabalhar nessa migração.
Uma primeira decisão que tomamos foi fazer com que a migração acontecesse em duas etapas: primeiro do Spring Boot 2.7 para o 3 e, depois, do 3 para o 4.
A ideia é que, dessa forma, o número de problemas fosse menor, afinal, é esperado que uma mudança da versão 2 pra 3 e da 3 pra 4 seja menor do que da versão 2 direto para a 4. Indicamos os guias oficiais de migração, pedimos a adaptação das APIs de Java EE para Jakarta.
Também explicamos como subir a aplicação, conectar ao MySQL, autenticar nas chamadas e onde olhar os logs, tudo isso para que ele tivesse muita autonomia para que ele conseguisse identificar erros, alterar o código, e conferir que deu certo.
Também deixamos claro como ele deveria validar que as coisas funcionavam. A regra era simples: antes de seguir para a próxima etapa, precisava ter código compilando, testes passando e a aplicação subindo. Descrevemos também quais comportamentos precisavam continuar funcionando e o agente não poderia continuar se eles quebrassem.
Como a migração seria algo que o agente precisaria trabalhar por horas, fizemos uso do /goal, recurso do Codex que faz com que a IA continue a trabalhar no problema de maneira ininterrupta até alcançar o seu objetivo.
Exigimos também que ela mantivesse um arquivo com o estado da migração: o que já tinha sido feito, os problemas em aberto, o que tinha sido tentado e qual deveria ser o próximo passo. Se a execução parasse, conseguíamos retomar de onde ela tinha parado.
Algumas mudanças que o agente fez no nosso código:
- Atualizou dependências e configurações relacionadas as novas versões do Spring
- Migrou Joda/Jadira para java.time
- Adaptou consultas do Hibernate, já que até a JPQL mudou entre versões
- Reescreveu partes da configuração de segurança no Spring Security
- Atualizou o Java de 21 para 25. Esse aqui foi um efeito indireto da migração, e que pedimos de maneira explícita para o agente.
Essa primeira passada do agente levou 2 horas e 33 minutos, entre execuções, interrupções e retomadas, até que a aplicação finalmente subisse e algumas páginas começassem a responder.
Nesse momento, o time se mostrou muito animado, pois já era bem mais longe do que tínhamos chegado nas tentativas anteriores. Mas bastou começar a navegar para perceber que ainda havia bastante coisa quebrada.
Segundo passo: aumentando cobertura de testes e disparando agentes para corrigir problemas
Os nossos testes automatizados ajudaram o agente nessa primeira passada, mas eles não foram suficientes. Precisávamos de mais guardrails para guiar o agente na migração. O passo seguinte foi então aumentar a cobertura da validação.
A plataforma da Alura já tinha uma lógica de execução de diversos endpoints, que é usado para fazer warm-up de novos containeres e aquecer a JVM. Decidimos reaproveitar a estrutura e transformá-la também em uma suíte de teste.
Com a ajuda de agentes novamente, expandimos as chamadas para todos os endpoints GET elegíveis, deixando apenas os jobs assíncronos de fora.
O agente elencou todos os endpoints disponíveis na aplicação. Então, endpoint por endpoint, o agente fazia uma chamada HTTP, e analisava tanto a resposta HTTP quanto os logs produzidos pela aplicação.
Um 404, por exemplo, podia significar simplesmente que determinado conteúdo não existia no banco local, mas também podia ser consequência de uma rota quebrada ou de uma exceção durante o processamento.
A IA, com acesso a tudo que precisava, conseguia validar por conta própria o resultado daquele endpoint e decidir por conta própria se precisava adicionar dados no banco ou refazer a chamada, até garantir que aquele endpoint estava funcionando na nova versão.
Essa etapa consumiu cerca de nove horas de execução da IA, além do acompanhamento e dos testes manuais.
O warm-up foi ótimo, pois o agente conseguiu percorrer boa parte da aplicação em poucas horas, e com isso, resolver uma outra grande leva de pontos que ainda precisavam ser migrados. Mas ele também deixou clara uma limitação: chamar um endpoint de maneira automática não é a mesma coisa que usar o produto.
Terceiro passo: a validação manual
Nesse momento, as suítes automatizadas estavam passando e os endpoints cobertos pela validação funcionavam dentro dos critérios que definimos.
Decidimos então que era hora de colocar pessoas para garantir que tudo estava funcionando do ponto de vista de usuário. Decidimos então parar o time por dois dias para testar manualmente.
Afinal, era agora ou nunca. Parar o time é sempre uma decisão difícil pois o backlog cresce rápido, mas ao mesmo tempo, estávamos tão perto de realizar algo que até então vivia apenas na imaginação.
Dividimos as funcionalidades entre as pessoas e times que conheciam cada área. Testamos de maneira manual funcionalidades como compra, renovação, criação de cursos, progresso do aluno, certificados, avaliações, relatórios, login e ferramentas de IA.
Os devs, claro, encontraram um novo conjunto de problemas. Esses problemas estavam espalhados por partes bem diferentes da aplicação. Alguns eram ajustes pequenos. Outros exigiam entender melhor mudanças no comportamento do Spring, do Hibernate ou na comunicação entre serviços.
O ciclo de trabalho nessa fase acabou sendo sempre parecido: reproduzir, entender, corrigir, testar e, quando fazia sentido, usar a IA para procurar outros lugares do código com o mesmo padrão.
Somando o esforço informado pela equipe, foram 209 horas entre testes, investigação, correções, revisão, pareamentos e acompanhamento do deploy. E isso com uma suíte que já tinha passado de 23 mil testes.
Alguns bugs ajudam a mostrar melhor o tipo de coisa que encontramos:
- Telas que quebravam por causa de uma barra. O Sprint Boot 2 por padrão aceitava URLs com e sem / (barra) no final, levando pro mesmo controlador. Esse comportamento mudou no Spring Boot 4. Tivemos problemas semelhantes em partes do sistema que ainda usavam JSP. Implementamos uma normalização que preservava método e corpo das requisições POST.
- Consultas que o Hibernate deixou de aceitar. Encontramos queries que passavam entidades onde eram esperados IDs, comparavam tipos incompatíveis ou geravam colunas com aliases repetidos. Isso afetava relatórios, movimentação de membros entre equipes e gráficos de atividade. Sempre que corrigíamos um caso, procurávamos consultas parecidas no restante do código e testávamos contra um MySQL real.
- Mudança de comportamento do @Transaction. Alguns serviços chamavam métodos transacionais da própria classe. Como essa chamada não passa pelo proxy do Spring responsável pela transação, tivemos que corrigir esses pontos de entrada e procurar outras ocorrências. Nesse caso, a migração não introduziu exatamente um novo problema. Ela expôs uma fragilidade que já existia.
- Logins e operações assíncronas. SAML deu bastante trabalho, principalmente na sessão e nos cookies enviados no retorno do provedor de identidade. Também encontramos problemas de autorização nas continuações assíncronas usadas pelo chat e pela exportação de relatórios. A primeira requisição funcionar não queria dizer que o restante do fluxo estava passando corretamente pelas regras de segurança.
- Quebra de contratos entre serviços. No Scratch, usado nos projetos de programação visual da Alura Start, os testes de serialização passavam, mas salvar um projeto quebrava. Quando rodamos nossa aplicação Java junto do serviço em Django, descobrimos que o JSON estava sendo enviado em chunks, sem Content-Length. O parser do outro lado acabava tratando o corpo como vazio. Passamos a enviar bytes com tamanho conhecido e criamos um teste usando um servidor HTTP real. O teste que validava apenas o objeto serializado nunca teria encontrado esse problema.
Quarto passo: o deploy controlado e monitoramento constante
Estava chegando a hora da verdade. Fizemos o rollout aos poucos, deixando ambas versões, nova e antiga no ar, e colocamos pausas manuais entre as etapas para olhar erros e testar a aplicação.
No primeiro minuto de deploy, com a nova aplicação ainda só recebendo tráfego nosso, descobrimos que precisaríamos também que separar as chaves de cache e sessão no Redis, já que as versões antiga e nova armazenavam dados incompatíveis. Afinal, tanta coisa mudou, que nem os dados guardados no cache eram os mesmos entre as aplicações.
Gastamos a manhã inteira validando de maneira manual a aplicação. O Datadog foi nosso grande amigo. Estávamos o tempo todo de olho em possíveis erros e aumento de latência.
Não encontramos problemas graves, e decidimos virar a chave completamente na hora do almoço, que é historicamente um horário com menos acesso a nossa plataforma.
Mesmo assim, depois da liberação para todos os usuários, apareceu um problema no login via Google. Tínhamos passado bastante tempo testando SAML. O fluxo de Google, porém, escapou. Usuários de escolas do Paraná que já estavam deslogados ficaram sem conseguir entrar por cerca de 30 minutos, até conseguirmos corrigir.
O placar final: alguns números da empreitada
Para sumarizar, alguns números dessa migração:
- 2.417 arquivos foram modificados em uma PR com 101 commits
- 13,5 mil linhas foram adicionadas
- 7,5 mil linhas foram removidas
- Tudo isso empacotado em 101 commits
- 250 mensagens na thread dedicada do Discord e incontáveis conversas nos canais de voz
- 1.2 bilhões de tokens gastos (na conta principal usada na migração, sem contar os tokens gastos nas outras contas, que não temos como medir)
- 209 horas dos nossos devs estressando a aplicação migrada
Lições aprendidas
Algumas coisas que funcionaram bem e que vamos repetir em uma próxima migração.
- Gaste tempo detalhando o prompt da migração. Nosso prompt foi bem detalhado, explicando as etapas da migração, o que precisa continuar funcionando, como executar o sistema e quais critérios definem que uma etapa terminou. Isso guiou bem a IA no trabalho.
- Dê à IA um ciclo de trabalho executável. Subir a aplicação, fazer uma requisição, ler os logs, corrigir e tentar de novo, funcionou muito melhor do que simplesmente pedir “migre para Spring Boot 4”, que havíamos tentado no passado mas sem sucesso. O /goal do Codex conseguiu trabalhar ininterruptamente nele.
- Crie mecanismos de verificação. No nosso caso, o warm-up virou uma ótima ferramenta de validação. No entanto, ele claramente não foi suficiente para que a IA conseguisse de maneira autônoma fazer o serviço. Não tínhamos uma bateria de testes que realmente executasse a aplicação de maneira end-to-end, e portanto, optamos naquele momento por envolver pessoas ao invés de tentar criar uma. Ter uma bateria dessas poderia ter nos ajudado a gastar ainda menos horas com pessoas.
- Quando encontrar um bug, procure a família dele. Uma query incompatível provavelmente não é a única. Um padrão antigo de URL também pode aparecer em outros lugares. A IA é especialmente útil para procurar essas repetições no código.
- Reserve tempo para quem conhece o produto testar. Não dá para delegar toda a validação para IA ou para testes automatizados. Jornadas completas, variações de login, operações de escrita e estado dos dados depois de uma ação precisam ser usados de verdade.
No nosso caso, a IA tornou uma migração grande o suficiente para ser atacada de uma forma que antes não tinha funcionado. Mas ela não eliminou o trabalho do time.
Na verdade, boa parte do trabalho mudou de lugar: menos tempo fazendo alterações mecânicas e mais tempo definindo critérios, investigando problemas, testando o comportamento real do sistema e decidindo se cada correção fazia sentido.
E você, já tentou fazer uma migração desse tamanho com ajuda de IA?
Agradecimentos: Apesar desse post ter sido escrito por mim, Maurício, o trabalho foi conduzido de maneira maestral pelo Carlos Eduardo Muller, nosso desenvolvedor especialista. O Carlos também coordenou todo o trabalho junto com os mais de 35 devs que participaram das validações manuais e acompanhou o deploy.