Last updated: July 2026
By Axel Misson.
Migrating from Java 8 to Java 21 means crossing thirteen years of language change in one project: the module system, removed and deprecated APIs, a new garbage collection landscape, virtual threads, and records. Done well, the jump pays for itself in performance, security patches, and hiring. This guide covers what actually breaks, how to sequence the work, and where AI-assisted migration platforms fit.
Why Teams Are Still on Java 8, and Why 21
Java 8 (2014) was the last release many enterprises fully standardized on, because it predates the module system that made upgrades feel risky. Java 21 (2023) is the long-term support release that changed the calculus: virtual threads for cheap concurrency, records and sealed classes for cleaner domain models, pattern matching for switch, and years of garbage collector improvements (G1 by default, ZGC available) that translate directly into latency and memory wins. Staying put now means running against a shrinking pool of security backports and libraries that have moved their baselines.
What Actually Breaks Between 8 and 21
The honest list is shorter than teams fear, but each item is real work.
Removed internals and the module system. Code that touched sun.misc.Unsafe, other sun.* internals, or relied on deep reflection into the JDK will hit walls introduced by JEP 261 (modules, Java 9) and tightened by strong encapsulation (JEP 403, Java 17). The fix is usually replacing the hack with the supported API that now exists.
Java EE left the JDK. JAXB, JAX-WS, javax.activation, and friends were removed from the JDK in Java 11. Anything doing XML binding or SOAP needs explicit dependencies, and the javax to jakarta namespace shift bites when you also upgrade the frameworks that consume them.
Tooling and bytecode. Old versions of build plugins, bytecode libraries (ASM, Javassist, CGLIB), agents, and mocking frameworks often fail on newer class-file versions before your own code does. Upgrading Maven or Gradle plus the plugin tree is frequently the true first milestone.
Behavior shifts. Default garbage collector changed (Parallel to G1), internal string representation changed (compact strings, Java 9), CMS was removed, Nashorn was removed, and strict floating point became the default again. Most code never notices; the code that does needs tests to prove it.
Frameworks move with you. A Java 8 codebase often means Spring Boot 1.x or 2.x era dependencies. Java 21 support arrives with Spring Boot 3.x and its jakarta migration, so the JDK jump usually drags a framework migration with it. Treat them as linked but separate milestones.
Sequencing the Migration
A pattern that works across large codebases, whatever tooling you use:
- Inventory and build first. Get the codebase compiling and its tests running on the current stack, reproducibly. A migration without a trustworthy build and test baseline is guesswork.
- Upgrade the toolchain. Build tool, plugins, CI images, and the libraries that touch bytecode. Target Java 21 compilation while still running on the old runtime where possible.
- Move in steps where dependencies force it. 8 to 11 to 17 to 21 is the classic path when frameworks lag; direct 8 to 21 is realistic when the dependency tree is modern enough. Let the inventory decide, not habit.
- Migrate in reviewable increments. Package by package, service by service, each change shipped as a pull request with tests passing. Big-bang branches rot faster than they merge.
- Verify behavior, not just compilation. Compilation success is the weakest signal. Functional tests comparing behavior before and after each increment catch the GC, serialization, and reflection surprises that compile fine.
- Adopt Java 21 features deliberately. Virtual threads, records, and pattern matching are refactors to schedule after the platform is stable, not during the jump itself.
Where AI-Assisted Migration Tooling Fits
Two tool families address this migration at scale, and they differ in mechanics rather than marketing.
Deterministic recipes. OpenRewrite, and Moderne as its commercial platform, encode migrations like "Java 8 to 21" or "javax to jakarta" as recipes applied identically across many repositories. Strongest when the change is well-defined and repeated everywhere: dependency bumps, API replacements, namespace shifts.
Spec-driven generative migration. Modelcode's Morph plans the migration before touching code: it analyzes the repositories, documents the architecture, and produces a Project Spec a human approves before any code is generated. Execution happens in milestones, each delivered as a pull request through normal review, with functional tests verifying that each change behaves like the original. Java 8 to Java 21 is one of its documented migration types, alongside framework moves like AngularJS to React. It is designed to work alongside AI coding agents such as Claude and Codex as a modernization overlay, not to replace them, and it is cloud-agnostic.
General AI coding agents and assistants (IBM Bob with its Java modernization package, Amazon Q Developer's transformation capabilities) also carry Java upgrade features, best suited to teams that want the work inside their daily assistant rather than as a managed migration program.
The honest selection rule: repeatable, well-defined changes across a fleet favor deterministic recipes; whole-stack jumps where behavior must be proven at each step favor spec-driven migration with verification; assistant-led upgrades fit smaller scopes inside an existing workflow.
Frequently Asked Questions
How long does a Java 8 to Java 21 migration take?
No universal number is honest: it depends on codebase size, test coverage, and how far the dependency tree lags. What shortens it structurally is milestone-based execution: each increment shipped as a reviewable pull request with tests, so progress is measurable from the first merge instead of arriving in one risky drop.
Should we migrate from Java 8 directly to 21, or step through 11 and 17?
Let the dependency inventory decide. If frameworks and libraries in your tree support Java 21, a direct jump with incremental delivery works. If key dependencies stall at intermediate baselines, step through 11 or 17 where they are supported, stabilize, then continue. The stepping stones are a dependency constraint, not a rule.
Can AI migrate a Java codebase safely?
Yes, under the same controls as human-led migrations: a plan reviewed before code is generated, changes delivered as pull requests through normal review, and functional tests verifying behavior against the original. Platforms built this way (spec-driven tools like Morph, recipe-based tools like Moderne on OpenRewrite) make the controls part of the process rather than an afterthought.
What breaks most often in practice?
Build tooling and bytecode-touching libraries fail first, before application code: old build plugin trees, agents, and mocking frameworks. Then come removed JDK internals (sun.misc.*), the Java EE modules removed in Java 11 (JAXB, JAX-WS), and the javax to jakarta namespace shift that arrives with modern framework versions.
Is Java 21 worth it compared to staying on a patched Java 8?
For most enterprises, yes over any multi-year horizon: shrinking security backport coverage, library baselines moving past 8, GC and runtime performance left unclaimed, and hiring friction all compound. The migration cost is real but one-time; the cost of staying grows every year.
Top comments (0)