Upgrading legacy tooling one step at a time

TL;DR - We had old runtimes, old frameworks and an Android app stuck on an old toolchain. I didn’t upgrade everything at once. I made a list, sorted it by risk, and upgraded one layer at a time with the pipeline as the safety net. AI did a lot of the mechanical work; the tests decided when it was done.


Part of the legacy wasn’t the code itself. It was everything around it: the Play Framework backend (which I wrote about in the Spring Boot migration post), old runtime and library versions, frontend packages with known vulnerabilities, and an Android app that broke when a customer’s RFID handheld got a system update. Nothing was on fire, but every new library we wanted needed a newer version of something.

The inventory

I started with a plain table: each project, its runtime, framework, build tool, and how far behind each one was. Just writing it down showed the order. Things with security issues first. Things that blocked other upgrades next. Cosmetic upgrades last.

One layer at a time

The rule was never to upgrade two layers in the same MR. Runtime first, with the same framework. Then the framework. Then the libraries. When something breaks, I know which change did it.

flowchart LR
  Inv[Inventory] --> Sec[Security fixes]
  Sec --> Runtime[Runtime: Java / Node]
  Runtime --> Framework[Framework major]
  Framework --> Libs[Libraries]
  Libs --> Build[Build and CI images]

The Android case

The handheld app was the most painful. A device update on the customer side brought a newer Android version, and our old Kotlin and Gradle setup started failing. Here I had to go further than planned: newer Gradle, newer Kotlin, newer target SDK. I tested on the real devices the customers use, not just the emulator, because the hardware scanner behaves differently.

Where AI helped

Upgrades are full of mechanical changes: renamed packages, deprecated APIs, new config formats. I gave the AI the migration guide and the compiler errors and let it propose the changes, one module at a time. It was fast and mostly right. When it was wrong, the tests caught it. Without tests, I wouldn’t have trusted it with this.

Keeping it from happening again

After the big catch-up, I set up an automated dependency check in the pipeline and a monthly slot to apply minor updates. Small updates every month are cheap. A three-year jump is a project.