A guide to Java, Linux, and other Technology topics

Step-by-Step: Migrating Legacy Apps to Jakarta EE 10 and Java 21

For enterprise software architects, the transition from older Java Enterprise Edition (Java EE) and Java 8/11 baselines to Jakarta EE 10 and Java 21 LTS is more than a routine upgrade. It is a fundamental shift in how enterprise applications are built, scaled, and maintained. This migration addresses the highly publicized namespace change from javax to jakarta while unlocking the massive performance potential of Java 21's Virtual Threads.

This guide provides a comprehensive blueprint for tackling the migration process, avoiding common pitfalls, and future-proofing your enterprise stack.

The Great Namespace Shift: javax.* to jakarta.*

The most infamous hurdle in modernizing legacy Java applications is the transition of enterprise APIs from the javax.* namespace to jakarta.*. Because this change breaks binary compatibility, a simple "find and replace" across your codebase will almost certainly result in compilation errors.

Understanding the Scope

Not all javax packages moved. Core Java SE packages, such as javax.sql and javax.crypto, remain unchanged. The shift only affects enterprise specifications, including:

  • javax.servlet.* becomes jakarta.servlet.*
  • javax.ws.rs.* becomes jakarta.ws.rs.*
  • javax.persistence.* becomes jakarta.persistence.*
  • javax.enterprise.* becomes jakarta.enterprise.*

Automating the Refactoring

Manually rewriting thousands of import statements and XML configurations is error-prone. Instead, leverage automated refactoring tools:

  • OpenRewrite: Utilizing the Migrate to Jakarta EE 10 recipe, OpenRewrite can automatically update import statements, modify Maven POM files, and adjust configuration files across large codebases.
  • Eclipse Transformer: If you rely on third-party JARs that have not yet been updated, the Eclipse Transformer can modify compiled bytecode, altering javax references to jakarta inside the legacy JARs during the build process.

Taming the Build: Dependency Management in Maven

Migrating the codebase is only half the battle. Your build system will likely be the source of significant friction as you resolve transitive dependencies pulling in legacy artifacts.

Updating the BOM and Core Dependencies

Begin by updating your POM file to point to the Jakarta EE 10 API. Remove legacy javaee-api dependencies.

<dependency>
  <groupId>jakarta.platform</groupId>
  <artifactId>jakarta.jakartaee-api</artifactId>
  <version>10.0.0</version>
  <scope>provided</scope>
</dependency>
    

Handling Transitive Dependency Conflicts

Older libraries will attempt to pull in Java EE 8/9 APIs. You must actively exclude these to prevent classpath pollution. Run mvn dependency:tree to identify the culprits and apply exclusions.

<dependency>
  <groupId>com.legacy.library</groupId>
    <artifactId>enterprise-util</artifactId>  
    <version>2.1.0</version>  
    <exclusions>  
      <exclusion>  
        <groupId>javax.servlet</groupId>  
          <artifactId>javax.servlet-api</artifactId>  
      </exclusion>  
    </exclusions>  
</dependency>
    

Migrating the Presentation Tier: JSP, JSF, and Facelets

For legacy applications featuring a server-side rendered UI, migrating the presentation tier is one of the most critical steps. Jakarta EE 10 introduces significant breaking changes for older web interfaces, particularly regarding Jakarta Faces (formerly JavaServer Faces or JSF).

The Removal of JSP Support in Jakarta Faces 4.0

If your legacy application relies on JSP (JavaServer Pages) to render JSF components, this acts as a hard blocker. Jakarta Faces 4.0 (included in Jakarta EE 10) completely drops support for JSP as a view declaration language. All JSP-based JSF views must be rewritten using Facelets. Pure JSPs (without JSF tags) are still supported via Jakarta Server Pages, but modernizing the UI layer to Facelets is required for continued JSF compatibility.

Updating XML Namespaces for Facelets

Facelets pages (.xhtml) must be updated to use the new jakarta.* XML namespaces. The legacy http://xmlns.jcp.org/jsf/* or http://java.sun.com/jsf/* URIs will fail to resolve and break page rendering.

  • Core Components : Update xmlns:f="http://xmlns.jcp.org/jsf/core" to xmlns:f="jakarta.faces.core"
  • HTML Components: Update xmlns:h="http://xmlns.jcp.org/jsf/html" to xmlns:h="jakarta.faces.html"
  • Facelets Templating: Update xmlns:ui="http://xmlns.jcp.org/jsf/facelets" to xmlns:ui="jakarta.faces.facelets"

Additionally, ensure your faces-config.xml is updated to version 4.0 and references the standard Jakarta schemas to ensure the application server correctly processes your configuration.

Unleashing Java 21: Virtual Threads in the Container

Java 21 LTS introduces Virtual Threads (Project Loom), fundamentally altering the performance profile of Java applications. For years, application servers relied on a thread-per-request model that was limited by the OS threading capacity. To scale, developers were forced into complex reactive programming frameworks.

The Return of Synchronous Code

With Virtual Threads, millions of lightweight threads can be multiplexed onto a small pool of OS threads. When an application makes a blocking I/O call (e.g., querying a database via Hibernate), the JVM unmounts the Virtual Thread, freeing the OS thread to handle another request.

This allows developers to write straightforward, synchronous, blocking code while achieving the throughput of reactive architectures. To enable this in a modern application server (like GlassFish or WildFly), you simply need to configure the server's thread pool to utilize Virtual Threads.

Modern Language Features

Java 21 also brings expressive features that reduce boilerplate in enterprise business logic:

  • Record Patterns: Simplify data extraction from complex objects.
  • Pattern Matching for Switch: Write cleaner, more exhaustive conditional logic without excessive casting.
  • Sequenced Collections: Finally, standard interfaces for collections with a defined encounter order.

Application Server Configuration

Configuration Area Migration Strategy
XML Schemas Update root elements in web.xml, beans.xml, and persistence.xml to reference Jakarta namespaces and version 10/4.0 schemas
JNDI Bindings Verify that container-managed resources (DataSources, JMS queues) map correctly to the new application references.
Security Realms Migrate legacy security constraints to Jakarta Security APIs (e.g., replacing proprietary login modules with standard HttpAuthenticationMechanism).

Testing and Behavioral Shifts

While the API signatures are largely identical post-namespace shift, Jakarta EE 10 introduces subtle behavioral changes. Thorough integration testing is mandatory.

  • CDI (Contexts and Dependency Injection): CDI 4.0 changes the default bean discovery mode to annotated. If your legacy beans.xml used all, you must update the file or explicitly add bean defining annotations.
  • JSON-B and JSON-P: Pay attention to null serialization behavior and specific edge cases when mapping complex entities.
  • Deprecated Features: Ensure you are no longer relying on features pruned from the platform, such as older EJB entity beans or legacy JAX-RPC.

Conclusion

Migrating to Jakarta EE 10 and Java 21 is a rigorous process, requiring careful dependency management, extensive refactoring, and thorough container tuning. However, the modernization eliminates technical debt, drastically improves developer ergonomics, and unlocks unprecedented application throughput via Virtual Threads. By leveraging automated tools and a structured approach, enterprise teams can execute this transition smoothly and set a robust foundation for the next decade of software development.