How to Modernize a Legacy Java Application

A sequencing guide for modernising a long-lived Java system incrementally — assessment, characterisation tests, strangler-fig extraction and runtime upgrades — without a big-bang rewrite.

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

01Assessfacts, not vibes02Stabilisetests + build03Extractstrangler fig04Upgradejdk + framework05Operatereleases boring
Each stage lowers the risk of the next one. Extraction is where the visible progress happens, but it is only safe once the build is reproducible and the critical paths are pinned down by tests.

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:

  1. Get a reproducible build. Pinned dependencies, containerised toolchain, no machine-specific steps.
  2. 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.
  3. 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

LooseTangledCoupling to the rest of the systemStableConstantChange frequencyNotificationsReporting exportsPricing rulesBilling coreLegacy batch jobsReference data
The first extraction should be changed often enough to pay back quickly, and coupled loosely enough to move without dragging half the system behind it. Save the tightly-coupled core for when the team has done this three times.

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

Time boxOne quarter01Assess2–3 weeks02Stabilisetests, build, deploy03First slicebehind a facade04Runtimelts upgrade
An illustrative shape rather than a fixed plan. The point is that assessment is short, stabilisation is unglamorous but load-bearing, and the first extracted slice lands inside the quarter.

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.

Start a conversation

Turning this into an actual project?Let's talk.

Tell us what you are trying to build, fix or decide. If we are not the right team for it, we will say so and point you somewhere better.

Typical first step: a 30-minute call, then a short written assessment.