Skip to content
Migrating a legacy Java application from javax to jakartaBackend
Backend

The javax to jakarta Break: Why Your Old Java App Is Stuck, and What It Costs to Move

RRRavi Rai··10 min read

You have a Java application that runs the business. It was written years ago, it works, and the people who wrote it have moved on. You ask three firms what it would take to bring it up to date and you get three answers: upgrade it, rewrite it, and leave it alone. The spread between those quotes is enormous and none of them explains itself.

Underneath almost every one of those conversations is a single decision made by a standards body in 2020, and it is the reason a Java upgrade does not behave like other upgrades. It is worth understanding before you accept any quote, because it tells you which of the three answers is honest for your application.

A note on where we are speaking from, because it matters for how you read this. Java is a newer service line for us and we say so on our Java development page rather than implying a decade of it. What follows is the decision as we work through it, and the checks are ones you can run yourself in an hour without hiring anybody.

One rename, and the ecosystem split in two

When Java EE moved to the Eclipse Foundation it could not keep the javax package namespace, for trademark reasons rather than technical ones. Jakarta EE 9 renamed every one of those packages to jakarta. The classes did not change. The methods did not change. Only the import lines did.

java// Before
import javax.persistence.Entity;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;

// After
import jakarta.persistence.Entity;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.constraints.NotNull;

That looks like a find and replace, and in your own source code it very nearly is. The problem is everything you did not write. A library compiled against javax.servlet cannot run in a container that only provides jakarta.servlet, and no amount of editing your own imports changes that. So the rename is not a change you can make gradually. Every dependency in the tree has to cross at the same time, and any one of them that has not published a Jakarta-compatible release holds the whole application on the old side.

This is why a Java upgrade quote is either small or very large with little in between, and why the honest answer to what it costs starts with a question rather than a number: which of your dependencies has not crossed.

Where your application actually sits

Three numbers place you, and you can get all three today. The Java version you compile and run against, the framework major version, and the servlet container. They are not independent: each one gates the next.

  • Java 8. Still the most common version we are shown. It is long past the point where new framework versions support it, so nothing modern will run on it without moving the runtime first. Moving off 8 is the prerequisite for everything else, not a separate project you can defer.
  • Spring Boot 2.x. The last line that used javax. Open-source support for 2.7 ended in November 2023, so a 2.x application is running on a framework that no longer receives free security patches. Commercial extended support exists and is a real option if you need time, and your finance team should price it against the migration rather than assuming there is no choice.
  • Spring Boot 3.x and later. Requires Java 17 as a floor and uses jakarta throughout. There is no configuration that makes a 3.x application accept javax dependencies. The line between 2 and 3 is the line in this whole article.
  • Tomcat 9 versus 10. The same split, in the container. Tomcat 9 implements the javax servlet API and Tomcat 10 implements jakarta. A WAR built for one will not deploy on the other. WildFly, WebLogic and WebSphere each have their own version of this boundary, so check yours rather than assuming.
  • Struts, JSP and an application server. If this describes your application, the framework question is not which Spring Boot version to target. It is whether the thing is a Spring application at all, and the honest path is usually different from an upgrade.

Those five bullets place most applications. The two that follow decide the cost.

The checks, which you can run in an hour

None of this needs a consultant. It needs the repository and a terminal. If you cannot get the repository, that is itself the finding, and it is a bigger one than anything below.

bash# 1. What you actually compile and run against
$ java -version
$ mvn -v 2>/dev/null || ./gradlew -v

# 2. The framework version, from the build file rather than from memory
$ grep -A2 'spring-boot-starter-parent' pom.xml
$ ./gradlew dependencies --configuration runtimeClasspath | grep -i 'spring-boot'

# 3. How much of YOUR code touches the renamed packages.
#    This number is usually smaller than people fear.
$ grep -rl 'javax\.' src/main/java | wc -l
$ grep -rho 'javax\.[a-z]*' src/main/java | sort | uniq -c | sort -rn

That last command is the one worth running first, because it separates the two kinds of javax import and they have nothing to do with each other. Packages that moved to Jakarta, such as javax.persistence, javax.servlet and javax.validation, are the migration. Packages that are part of the JDK itself, such as javax.sql, javax.crypto, javax.net and javax.xml, never moved and never will. An estimate that counted every javax line the same way is an estimate that has not been done.

bash# 4. The dependency tree, which is where the real answer is.
#    You are looking for anything unmaintained: the last release date matters
#    more than the version number.
$ mvn dependency:tree -Dscope=compile > /tmp/tree.txt
$ wc -l /tmp/tree.txt

# 5. Known vulnerabilities in what you are running today
$ mvn org.owasp:dependency-check-maven:check

# 6. Is there a test suite, and does it currently pass?
#    A migration without tests is a rewrite with extra steps.
$ mvn -q test 2>&1 | tail -20
$ find src/test -name '*Test*.java' | wc -l

Check six is the one that decides which path is available to you. A migration is a large mechanical change across a codebase, and the only thing that tells you it worked is a test suite that passed before and passes after. Without one you are not migrating, you are rewriting and hoping, and the quote should say so.

The four honest paths, and who each is for

1. Stay, and pay for support

Keep the application where it is and buy commercial support for the framework version you are on. This is the option nobody proposes because nobody bills for it, and it is sometimes correct: an application that is stable, not growing, and behind a firewall does not always justify a migration budget. It buys time, not a future, and the clock does not stop.

2. Move the runtime only

Take the Java version up without touching the framework. Moving from Java 8 to Java 11 or 17 on an older framework is usually smaller than people expect, and it gets you a supported runtime, real performance gains from a modern garbage collector, and a starting point for everything after. It does not solve the namespace problem. It is the first step of path three, done separately so it can be shipped and verified on its own.

3. Migrate across the namespace

The full move: runtime, framework major version, and every dependency across to Jakarta at once. It is mechanical, which is the good news, and there is real tooling for the mechanical part. OpenRewrite publishes recipes for exactly this migration, and the Eclipse Transformer will rewrite bytecode in a dependency that has no Jakarta release. What neither tool does is fix a library whose maintainer stopped publishing, and that is where the estimate lives.

bash# The mechanical part has tooling. Run it on a branch, read the diff,
# and do not let anyone tell you the tool finishes the job.
$ mvn org.openrewrite.maven:rewrite-maven-plugin:run \
    -Drewrite.activeRecipes=org.openrewrite.java.migrate.jakarta.JavaxMigrationToJakarta

# Spring's own property names moved too. This reports the ones
# that changed rather than failing silently at startup.
# Add spring-boot-properties-migrator to the build, run once, then remove it.

4. Strangle it

Leave the old application running and build new functionality beside it, moving traffic across one route at a time behind a gateway or proxy. This is the right answer more often than either a big-bang migration or a rewrite, particularly when the application is large and the business cannot stop. It is also the slowest to show a finished result, which is why it loses arguments to a rewrite that sounds faster and is not.

A rewrite is deliberately not on that list as its own path. It is what happens when the answer to check six is no tests, the answer to check four is several abandoned dependencies, and the people who understood the business rules have gone. That is a real situation and sometimes a rewrite is genuinely the cheaper option, but it should be a conclusion you are led to by those checks rather than a proposal you are given in week one.

What a quote is not telling you

  • Whether the estimate counted JDK javax packages as migration work. Ask what the split was. If nobody can tell you, the number came from a line count rather than from reading the code.
  • Which dependency is the blocker. Every one of these migrations has one library that decides the shape of the project. Ask which one it is. A firm that has looked will name it in a sentence.
  • What happens to the test suite. If there is no suite, the migration includes writing one, and that is most of the work rather than a line item at the bottom.
  • Whether the runtime move is separable. It usually is, it de-risks everything after it, and it can often ship in its own release. A plan that bundles everything into one cutover is a plan with one very bad day in it.
  • What the rollback is. On the day you deploy a migrated application, the question is not whether it works. It is how quickly you can be back on the old one if it does not.

Where we fit, said plainly

We build Java and Spring Boot backends, and it is a newer service line for us than Laravel or Node, which is what the service page says rather than something we would rather you did not notice. What we do bring to a legacy migration is the part that is not Java-specific: we have taken over other people's codebases repeatedly, and the day-one audit we run on an inherited application is the same sequence whatever it is written in, because the questions that decide a takeover are about what was written down rather than about the language.

If your application is a Struts monolith on WebSphere, hire a firm that does only that. If it is a Spring application that has fallen behind, that is work we can do, and the checks above will tell you which one you have before either of us guesses.

Run the six checks above and send us what they say. We will tell you which of the four paths fits, and if the honest answer is a specialist rather than us, we will say that instead.

Talk to us about your Java application

Frequently asked questions

Can I migrate from javax to jakarta gradually, package by package?
No. A library compiled against javax.servlet cannot run in a container that provides only jakarta.servlet, so the whole dependency tree crosses together. The Eclipse Transformer can rewrite bytecode in a dependency that has no Jakarta release, which is the nearest thing to a partial move, but it is a workaround rather than a gradual path.
Do all javax imports need to change?
No, and this is the most common estimating error. Packages that belong to Jakarta EE moved, such as javax.persistence, javax.servlet and javax.validation. Packages that belong to the JDK did not, such as javax.sql, javax.crypto, javax.net and javax.xml. Counting both together inflates the estimate considerably.
Should I upgrade Java and the framework in the same release?
Usually not. The runtime move is separable, smaller than people expect, and gets you a supported JVM on its own. Shipping it first means that when the framework migration lands you are debugging one change rather than two.
Is a rewrite ever cheaper than migrating?
Sometimes, and the deciding factors are testable rather than matters of taste: whether a test suite exists and passes, whether any blocking dependency is abandoned, and whether anyone still understands the business rules. Get answers to those three before accepting a rewrite proposal.
RR
Written by
Ravi Rai

Founder of buildbyravirai, a web development agency based in Noida, India. 5+ years shipping Next.js, WordPress, Shopify, and Laravel projects for clients in India, USA, Canada, and the UK.

Backend and APIs

Need this backend built properly the first time?

Billing engines, notification services, integrations with the accounting stack you already run. We build the unglamorous parts that decide whether the product holds up at volume.