Most legacy Java systems are not badly written. They are systems that were written under different constraints, then extended for a decade by people who have since left. The code is not the problem. The cost of change is.
Modernisation, done properly, is a programme to lower that cost in increments — each of which is independently valuable.
Figure 01 — the sequence that keeps every step reversible
Step 1: Assess before you promise anything
An assessment should produce facts, not impressions. The minimum set worth gathering:
- Runtime and dependency risk. Which versions are past end of support, which have known CVEs, which block certification.
- Change hot spots. Run history analysis over the repository. The files changed most often are where maintainability costs are actually being paid.
- Test reality. Not the coverage number — whether the tests would catch a regression in the critical business paths.
- Deployment path. How long a release takes, how many people it needs, and how it is rolled back.
- Knowledge concentration. Which parts of the system only one person understands.
That last one is usually the real risk, and it never appears in a static analysis report.
Step 2: Stabilise before you improve
Refactoring without a safety net converts a maintainability problem into an incident. Before changing structure:
- Get a reproducible build. Pinned dependencies, containerised toolchain, no machine-specific steps.
- Write characterisation tests around the highest-value paths. These do not assert what the code should do — they capture what it currently does, so unintended changes are visible.
- Automate deployment far enough that shipping a small change is boring.
Only now is the system safe to restructure.
Step 3: Extract in slices, behind a facade
The strangler-fig pattern works because every step is reversible:
- Put a routing layer in front of the functionality you intend to move.
- Build the replacement behind it.
- Shift a small percentage of traffic. Compare outputs — shadow traffic is cheap insurance.
- Increase the percentage. Keep the old path warm until you are certain.
- Delete the old path only after the new one has carried real production load for a meaningful period.
Choose the first slice for low coupling and high change frequency, not for how interesting it is.
Figure 02 — picking the first slice
Step 4: Upgrade the runtime once the risk has moved
Framework and JDK upgrades are far easier once the highest-risk code paths live in services with clean dependency trees. Doing the upgrade first means doing it against the worst version of the codebase.
A pragmatic order: Java LTS upgrade → build tooling → framework major version → library sprawl cleanup.
Figure 03 — a first quarter that ships something every month
What to avoid
- The two-year rewrite with no intermediate delivery. The most reliable way to fail. Business priorities will change before you ship.
- Microservices as the goal. Distribution is a cost you take on to solve a specific problem — usually independent scaling or independent deployment. If neither applies, a well-structured modular monolith is cheaper and faster.
- Freezing feature work “until modernisation is done”. The business will not accept it, and the freeze will be broken in month two anyway. Plan for parallel work from the start.
A reasonable definition of done
You are finished when releases are routine, the runtime is supported, an average change no longer requires the one person who remembers, and new engineers become productive in weeks rather than quarters.
Not when the architecture diagram looks fashionable.