Blog
Modernizing legacy Java: what to keep and what to change
Java itself is rarely the problem in legacy applications. Learn what to keep, what to change and where modernization should really start.
In many boardrooms, Java is still seen as yesterday’s technology, something you will eventually need to move away from.
That view does not hold up. Since 2017, a new version of Java has been released every six months. Java 21 and Java 25 are modern LTS releases. If you built an enterprise application in 2005, there was a good chance you used Java. Today, Java remains a logical choice for new business-critical applications.
That does not mean nothing needs to change. The system that has been processing your transactions for the past fifteen years is becoming increasingly difficult to modify safely. A release takes weeks. Knowledge of the WebSphere configuration rests with one experienced developer who will retire within the next few years.
There is clearly a need to modernize, but Java itself is rarely the problem.
Discussions about legacy Java tend to mix up three different things: the language and platform, the enterprise APIs, and the architectural style. Once you separate them, it becomes clear what is outdated, what remains perfectly usable, and where modernization should really start.
Three things to keep separate
The language and platform
Java as a language and runtime is evolving faster than ever. Records, sealed classes, pattern matching, and virtual threads have significantly modernized the platform in recent years.
Virtual threads, a standard feature since Java 21, make it possible to use the familiar thread-per-request model for large numbers of concurrent tasks without immediately resorting to complex asynchronous code. They do not automatically make every application faster, but they remove an important barrier to building scalable applications.
The legacy problem therefore does not lie in the language or platform.
The enterprise APIs
The specifications formerly known as J2EE and Java EE live on as Jakarta EE under the Eclipse Foundation. Jakarta EE 11 was released in June 2025 and introduced Jakarta Data, support for records, and improvements for using virtual threads, among other features.
The names and namespaces have changed, but many of the underlying standards and programming concepts remain relevant in modern enterprise Java. Frameworks such as Quarkus continue to build on Jakarta specifications. Spring also uses several APIs from that ecosystem.
The technology has continued to evolve. The real bottleneck usually lies elsewhere.
The architectural style
This is where the real problem usually lies: a tightly coupled application packaged as an EAR file and deployed on a heavyweight application server, with only a handful of releases each year.
That model was designed for a different era. It does not align well with continuous delivery, containers, and infrastructure that needs to scale quickly. As a result, small changes soon turn into major release projects.
The language is not outdated. The way the application is structured, deployed, and managed is.
What legacy Java really means: coupling and cohesion
In discussions about legacy Java, the monolith is often blamed first. But having a single deployable does not necessarily mean you have a poor architecture. For many organizations, a modular monolith is a perfectly sensible target architecture, provided its components have clear responsibilities and boundaries.
The real problem is too much coupling and too little cohesion.
Modules know too much about one another, while responsibilities are spread across different parts of the system. As a result, you cannot change, test, or deploy one component independently.
That is what makes a system legacy: not the age of the code, but the extent to which everything is tied together.
Consider this example.
An order platform was built in 2009. The business asks for a new discount rule in the catalog. On paper, it is a day’s work.
In practice, the catalog module writes directly to the invoicing tables. The reporting module reads from those same tables, and some of the discount logic is embedded in a JSF screen. One small change therefore requires regression testing across the entire system, a full release, and an overnight deployment window.
Java is not what makes that change complex. The direct dependencies and scattered logic do. You can create the same problem in any language.
Three generations of enterprise Java
To understand today’s landscape, it helps to distinguish between three generations. You will still encounter all three across the Belgian IT landscape.
Three generations, still side by side
-
1. The application server generation Single EAR on WebSphere, WebLogic or JBoss
-
2. The Spring generation Self-contained app with an embedded web server The mainstream of enterprise Java today
-
3. Cloud-native by design Container image, optionally a native executable Frameworks such as Quarkus
Today: all three in production
Milestones from the article
- 2014 Spring Boot arrives
- 2017 A new Java release every six months
- Java 21 LTS: virtual threads become standard
- June 2025 Jakarta EE 11
- Java 25 LTS
Generation 1: the application server generation
This generation includes J2EE and Java EE applications built with EJBs, servlets, JSP, or JSF and deployed as a single EAR on WebSphere, WebLogic, or JBoss.
These systems are often robust and transactionally reliable, but they are cumbersome to manage and slow to change. Many have been running for fifteen years or more and still do what they are supposed to do. The problem is that maintaining them becomes more expensive and riskier every year.
Generation 2: the Spring generation
These applications replaced the heavyweight EJB model with Spring and, from 2014 onwards, increasingly with Spring Boot.
The real benefit came not only from the framework itself, but above all from the architecture it enabled: self-contained applications with an embedded web server, explicit dependencies, and code that can be tested outside an application server.
This is the mainstream of enterprise Java today.
Generation 3: cloud-native by design
These architectures are built around containers, fast startup times, and elastic scaling, using frameworks such as Quarkus that were designed with those requirements in mind.
Quarkus moves part of the work from runtime to build time. Combined with native executables, this can result in very fast startup times and lower memory usage. That makes it attractive for certain cloud and serverless workloads, but it does not automatically make it the best choice for every application.
These three generations now exist side by side.
Modernizing does not mean leaving Java behind. It means moving deliberately from one model to another, at a pace and to a degree that suit your organization.
The order matters. First determine which architecture you need. Then choose the framework that best supports that decision. Not the other way around.
You can read more about which framework suits your context in Spring Boot or Quarkus.
Two clocks are ticking
If Java itself is not the problem, why not simply postpone modernization?
Because two risks increase the longer you wait.
The talent clock
Developers with in-depth knowledge of EJB 2, JSF, or a specific WebSphere configuration are becoming harder to find. At the same time, few new developers are deliberately choosing to work with such a stack.
The longer you wait, the smaller the window in which the people who understand the system can still guide its modernization. If that knowledge is lost before the work begins, the next team will have to do more than update the technology. It will also need to reconstruct why the system works the way it does.
The security pressure
Keeping versions up to date is first and foremost a security concern.
If your runtime, server, and libraries remain within supported versions, you can usually respond quickly to a serious vulnerability. If they have fallen years behind, you may need to complete several upgrades before a patch can even be applied. A pressing security issue can then suddenly turn into a major migration project.
According to New Relic’s 2024 State of the Java Ecosystem report, around 29% of the Java applications it monitored were still running on Java 8, a version released in 2014. At the same time, ISO 27001, NIS2, and stricter customer requirements are making unsupported or inadequately patched technology stacks increasingly difficult to justify.
The talent clock and security pressure reinforce one another. As an upgrade becomes more urgent, fewer people remain who understand the existing environment well enough to carry it out safely.
A JDK upgrade is modernization too
Modernization does not have to begin with a new framework or a different architecture. Moving to a supported JDK can already deliver substantial benefits.
Java places a strong emphasis on backward compatibility. Code that relies only on supported Java APIs often continues to work surprisingly well after an upgrade. Even so, moving from Java 8 to Java 21 is not always simply a matter of recompiling.
Outdated dependencies, reflection, internal JDK APIs, removed components, and changes in default behavior can all cause problems. A JDK upgrade is not a rewrite, but it still requires careful preparation, testing, and measurement.
The benefits are clear: a supported runtime, recent security updates, and access to new language and platform features. And all of that before replacing a single framework.
The conclusion: keep what works, change what holds you back
“Moving away from Java” can mean throwing away years of investment, accumulated domain knowledge, and a mature ecosystem without good reason.
In many cases, the better direction is to move away from the old deployment model instead.
Organizations that take a pragmatic approach to modernization retain the language and large parts of their business logic. They replace what is actually holding them back: an unsupported runtime, tight coupling, unclear module boundaries, and a release process that makes every change larger than it needs to be.
That starts with an honest look at what you have today. What is the cost of standing still, based on figures rather than gut feeling? We explore that in What standing still really costs.
Sometimes that assessment shows that it is better to wait. In that case, read When you should not migrate yet.
If you want to understand where the biggest bottlenecks lie in your own landscape, an objective second opinion on your architecture is often more useful than immediately setting aside a migration budget.
Further reading
This is the first article in a series based on the e-book Evolve, Don’t Rewrite.
The book picks up where this article ends, covering the business case, the decision framework for Spring Boot and Quarkus, the strangler fig pattern, tooling, the role of AI, and a four-phase roadmap. It also includes a chapter on when it is better not to migrate yet.
Pieter Vercammen is Managing Partner and Tech Lead at CraftCode.
< Keep reading />
More from our team
Explore more insights, tips, and deep dives from the CraftCode team.
Let's get to work!
Ready to turn your vision into reality?
Let's build the systems that power your growth.