Writing · Java

Spring Boot 2.7 End of Life: What the Upgrade to 3 Costs

What decides the cost, and how to check your own estate.

Two Java estates reported the same finding: an unsupported Spring version. One needed weeks of mechanical work to get current. The other had already done half the job and stalled on its largest services. The version number did not tell them apart.

The short answer. The version number tells you that you are behind. It does not tell you what catching up will cost. Five things in your own code decide that: whether your blocking libraries have a forward version, how entangled they are, whether javax references are import lines or inline in method bodies, how well tested the changing code is, and whether artifacts named jakarta.* really use the jakarta packages.

Dependabot, Snyk, OWASP Dependency-Check and endoflife.date all report which version you are on and its advisories, accurately and cheaply. What it costs to move is a separate question, and the answer to it is in your code, where a dependency list cannot see it.

Below are the two estates, what separated them, the five checks, and the shell commands that place your own codebase in about ten minutes. Both estates were assessed from their public repositories, and neither is named (the reason is at the end).

Two estates, one finding

EstateWhat the tools reportedWhat it was
Estate ASpring Boot 2.7.x, out of OSS support since 2023Weeks. Mechanical, and well covered by tests
Estate BSeveral Boot versions, none supportedHalf-done already, and the remainder is straightforward

Estate A: further behind, and a mechanical upgrade

Several hundred thousand lines of Java across dozens of modules, on Spring Boot 2.7.x: by version, the worse of the two. It had a test class for every few production classes. One dependency had no jakarta build, an abandoned API-documentation integration, but it touched very little code and had a drop-in replacement. The upgrade was weeks of mechanical work. Most of the effort went on checking the HQL and reconfiguring Spring Security; the rename itself was quick.

Estate B: half-migrated, with the largest services remaining

About ten services across several Spring Boot versions, none supported. Some had crossed to Boot 3 and the jakarta packages; the ones that had not were the largest and the most actively developed. The source shows where the migration stopped but not why, and what was left was in good shape. The remaining javax references numbered in the low hundreds, all of them import lines, which an OpenRewrite recipe handles. None of the remaining services used WebSecurityConfigurerAdapter, the most disruptive removal in Spring Security 6. Between them they had a few dozen entities and over a thousand test methods: a small, well-tested migration.

The third case: when no forward version exists

Some blockers have nothing to upgrade to. Apache last released Struts 1 in December 2008, and Axis 1 stopped around the same time. A web tier built on either cannot follow Spring 6 across the javax to jakarta rename, because nothing exists on the other side. The web tier has to be replaced, and that needs its own budget. Nothing in the dependency list tells you this.

OpenRewrite’s recipes would do most of the work on both estates above. On this third kind they rewrite what they can and stop. What you need to know in advance is whether the recipe can finish.

What each verdict means for a plan

The three cases call for three decisions: scheduled maintenance; a couple of sprints that stop an internal split getting worse; or a funded programme that will not fit a maintenance window. Mistaking one for another costs in both directions. Treat a replacement as a version bump and the migration stalls halfway, with the estate split across two namespaces. Treat a mechanical upgrade as a rewrite and you ask for a budget you did not need.

What decides the cost

The version number was the least informative fact. These mattered more:

  • Does a forward version exist for your blockers? For most libraries, yes. For the long tail that stopped in the 2000s, no, and the blocker then has to be replaced.
  • How entangled is the blocker? “We use Struts” and “Struts is a third of our Java files” carry different budgets.
  • Import lines or inline references? A javax import is a one-line automated change; a fully qualified reference in a method body is a hand edit.
  • Test coverage where the change lands? It decides whether you can verify the migration.
  • Do the artifact names match their namespaces? Jakarta EE 8 releases carry jakarta.* coordinates but javax packages. A dependency list counts them as migrated even though the code still uses javax.

Checking your own estate

None of this needs a tool. From the root of a Java or Kotlin repository:

# Only these javax packages moved to jakarta. javax.sql, javax.crypto, javax.tools and
# the rest of Java SE keep their names, so counting a bare "javax." tells you almost
# nothing: on one of our own already-migrated repositories it matches 17 files, of which
# 10 are Java SE packages that are never going to move.
EE='javax\.(servlet|persistence|validation|annotation|ws\.rs|inject|transaction|jms|mail|faces|ejb|xml\.bind)'
j() { grep -rE --include='*.java' --include='*.kt' --exclude-dir=build --exclude-dir=target --exclude-dir=out "$@" .; }

# Files to migrate, and how many import lines that is
j -l "$EE" | wc -l
j -h "^[[:space:]]*import( static)? $EE" | wc -l

# Fully-qualified references outside import lines: the expensive ones. Comments and string
# literals land here too, so read the hits rather than trusting the count.
j -n "$EE" | grep -vE ':[[:space:]]*import ' | wc -l

# The most disruptive removal in Spring Security 6
j -l 'WebSecurityConfigurerAdapter' | wc -l

# Mapping surface, and the HQL that Hibernate 6's parser rewrite actually bites
j -ho '@Entity\b' | wc -l
j -ho '@Query\b' | wc -l

# The safety net
j -ho '@Test\b' | wc -l

Keep the word boundaries. Without them @Entity also counts @EntityListeners and @EntityScan, @Test counts @TestConfiguration, and @Query counts every @QueryParam in a JAX-RS codebase, which can inflate it tenfold.

Then list your blockers: anything that uses the javax EE APIs and has had no release since the rename. Libraries that never touched those APIs are unaffected, however old. The counts give a verdict in this order:

A sequence of four checks. If every blocker has a forward version, if the javax references are all import lines, and if there are tests where the change lands, the work is a mechanical upgrade; a no at any step diverts to a larger piece of work. Does every blocker have a forward version? javax EE APIs with no release since the rename No — it has to be replaced, with its own budget Are the javax references all import lines? compare the import count with the outside-imports count Inline refs — hand edits on top of the recipe Are there tests where the change lands? @Test near your @Entity and @Query surface Thin — write tests before you migrate All yes — a mechanical upgrade. Schedule it.
What the counts mean. The first question decides the shape of the work; the rest only decide its size.

What this does not tell you

Whether any of it is exploitable. Advisories pile up on an unsupported version, but a CVE in code your application never calls is a different risk from one on a path it runs every request. Telling them apart needs the built artefact and a call graph. Do that before an advisory count sets the timeline: it usually shortens the urgent list.

Why these estates are unnamed

The figures are rounded. This site says where we have worked, so a count precise enough to match a repository would identify an estate, and naming an organisation’s unpatched versions is not the same as describing the pattern. Neither estate is a client, neither team was asked for anything, and both showed sound engineering: one had already done most of a migration that many estates have not started. The method is the same either way, and you can run it on your own code this afternoon.

We do this for a living: see what we do, or how we modernise legacy systems without a big-bang rewrite. If you would like a second opinion on your own estate, get in touch.

Have a system to build, or one that needs work?

Get in touch

Or follow new writing in a reader: Atom feed.