Skip to main content

Java 8 to Java 21 LTS Modernization Blueprint

Upgrade long-lived Java 8 applications to the Java 21 LTS runtime, adopting records, pattern matching, and virtual threads. Reflective libraries are upgraded before the JDK to survive strong encapsulation, and removed APIs are found early with jdeps.

From
Java 8
To
Java 21
Difficulty
Intermediate
Duration
14 weeks
Team Size
medium

What and Why

Java 8 is long past its prime and many libraries no longer support it. Java 21 is the current long-term-support (LTS) release, bringing virtual threads for cheap concurrency, records for concise data carriers, pattern matching for cleaner branching, sealed classes, and large performance and garbage-collector improvements. This blueprint upgrades Java 8 applications to Java 21. The jump spans the introduction of the Java module system and stronger encapsulation in newer JDKs, so it is a real project rather than a recompile, but it restores a supported, fast, modern runtime.

Phases

Assessment. Run jdeps, the JDK dependency analyzer, to find usage of removed or internal JDK APIs such as sun.misc.Unsafe and the removed Java EE modules. Inventory libraries and frameworks for Java 21 compatibility, paying special attention to anything reflective like older Spring, Hibernate, or bytecode-manipulation tools, since strong encapsulation in newer JDKs breaks naive reflection.

Dependency upgrade. Upgrade frameworks and libraries to Java 21-compatible versions first, before touching the JDK, because old reflective libraries fail at runtime on newer JDKs. Move off the Java EE modules removed from the JDK, such as JAXB and JAX-WS, onto their standalone Jakarta artifacts.

Runtime upgrade. Build and run on JDK 21, fixing compilation errors from removed APIs and stricter module encapsulation. Where a library truly needs access to internal packages, resolve it explicitly with --add-opens flags rather than masking the problem, so the dependency on internals is visible and tracked.

Feature adoption. Once the runtime is stable, and only then, adopt features that add value: records for DTOs, pattern matching for cleaner type-based branching, and virtual threads to simplify high-concurrency code that previously juggled thread pools. Doing this after stabilization keeps the runtime upgrade and the refactor from entangling.

Stabilization. Tune the garbage collector, choosing G1 or ZGC based on latency goals, re-baseline performance against the Java 8 numbers, and update CI to build and test on JDK 21.

Key Risks and Mitigations

  • API removals: Internal and Java EE APIs are gone from the JDK. Find them during assessment with jdeps and design replacements before compilation, not when the build breaks.
  • Dependency gaps: Old reflective libraries fail under strong encapsulation. Upgrade them before the JDK, never after, or the runtime upgrade stalls on cryptic reflection errors.
  • Behavioral changes: Garbage-collector and default-behavior shifts can change performance characteristics. Re-baseline and load-test before cutover so regressions are caught in advance.

Recommended Tooling

jdeps for dependency analysis, JDK 21 as a current LTS, Spring Boot on a Java 21-compatible version, Docker for packaging, and a CI pipeline building on the new JDK throughout the migration.

Success Metrics

Track cost reduction, since virtual threads and better garbage collection cut resource use, lead time once the code is back on a supported runtime, and performance measured against the Java 8 baseline.

Prerequisites

A meaningful test suite, a dependency inventory annotated with Java 21 support status, and a build and runtime environment with JDK 21 available.

Sequencing and Rollback

Sequence strictly: upgrade dependencies to Java 21-compatible versions while still building on Java 8, then switch the JDK, then adopt new language features only after the runtime is stable. Each stage is independently revertible, and separating the runtime upgrade from feature adoption keeps any regression attributable to one change. Re-baseline performance and run load tests on JDK 21 in a staging environment before production cutover, since garbage-collector behavior changes are the most likely source of a surprise that would otherwise force a rollback. Confirm the build, test, and runtime environments all provide JDK 21 before starting, and that any required --add-opens flags are recorded explicitly in the build, so the dependency on JDK internals stays visible and is paid down rather than hidden.