Most Java codebases in production today are still on Java 8, 11, or 17. Upgrading captures years of ecosystem improvements — but the work is tedious, repetitive, and easy to get wrong. This post is a copy-paste playbook of AI prompts (works in Claude, ChatGPT, Gemini, or Cursor) that take you stage-by-stage through every LTS jump: 8→11, 11→17, 17→21, and 21→25. Each prompt is described so you know exactly what it does and when to reach for it.
Java 8 → Java 11
This is the hardest jump because of the Jigsaw module system and the removal of the Java EE modules that were bundled with the JDK (JAXB, JAX-WS, CORBA, Activation, Annotations). Most upgrade pain — broken builds, ClassNotFoundException at runtime, split packages — happens here.
Prompt 8→11-1 — Find Removed Java EE APIs
What it does: Scans a file for imports of the Java EE packages that were removed from the JDK in Java 11 (javax.xml.bind, javax.xml.ws, javax.annotation, javax.activation, org.omg). For each hit it lists the standalone Maven/Gradle dependency that replaces it, so you can fix pom.xml and the import in one step.
When to use it: First thing after switching the compiler to Java 11 — most build failures trace back to these removed packages.
Scan this file for imports of javax.xml.bind, javax.xml.ws, javax.annotation, javax.activation, or org.omg. For each finding, list the standalone Maven/Gradle dependency that replaces it (jakarta.xml.bind-api, jaxws-ri, jakarta.annotation-api, etc.) and show the updated import statement.
Prompt 8→11-2 — Replace sun.* Internal APIs
What it does: Finds every usage of sun.*, com.sun.*, and jdk.internal.* and replaces them with public API equivalents. Where no direct replacement exists, it flags the line and suggests a workaround using java.lang.reflect or java.lang.foreign.
When to use it: When the Java 11 compiler emits “warning: [restricted]” or your module descriptor needs --add-opens flags.
Find every usage of sun.*, com.sun.*, or jdk.internal.* in this file. Replace each with a public API equivalent. If there is no replacement, flag the line and suggest a workaround using java.lang.reflect or java.lang.foreign.
Prompt 8→11-3 — Modernise String and Collection APIs
What it does: Rewrites code to use the new Java 11 String methods (isBlank, strip, lines, repeat), collection factory methods (List.of, Map.of, Set.of), Optional.isEmpty, and Files.readString/writeString. These are clean wins — shorter and more readable than their Java 8 equivalents.
When to use it: After the build is green on Java 11 — this is the modernization pass, not the fix pass.
Modernize this Java 8 code to use Java 11 APIs: String.isBlank, String.strip, String.lines, String.repeat, Collection.toArray(IntFunction), List.of / Map.of / Set.of, Optional.isEmpty, and Files.readString / Files.writeString. Show before/after for each change.
Prompt 8→11-4 — Migrate to var
What it does: Replaces explicit local variable type declarations with var wherever the right-hand side makes the type obvious. Correctly avoids var for fields, method parameters, lambdas, and method references (where it is not permitted or reduces clarity).
When to use it: In any file that has long, repetitive type declarations like Map<String, List<MyVeryLongClassName>> map = new HashMap<>().
Replace explicit local variable types with 'var' wherever the right-hand side makes the type obvious. Do not use var for fields, method parameters, or when the RHS is a lambda or method reference. Show each replacement with a brief comment explaining why var is appropriate there.
Prompt 8→11-5 — HttpURLConnection → java.net.http.HttpClient
What it does: Rewrites the old verbose HttpURLConnection boilerplate to use the Java 11 java.net.http.HttpClient API. Chooses the async sendAsync() form if the original code was blocking inside a thread, and preserves error-handling semantics.
When to use it: Any file still using HttpURLConnection, OkHttp wrappers for simple cases, or Apache HttpClient for basic GET/POST calls.
Rewrite this HttpURLConnection code to use the Java 11 java.net.http.HttpClient API. Use the async sendAsync() form if the original was blocking inside a thread pool. Preserve error-handling and timeout semantics, and show the updated import statements.
Java 11 → Java 17
Java 17 is the release where records, sealed classes, pattern matching for instanceof, and text blocks all became final. Most of this jump is modernization rather than breakage — the breakage surface (strong encapsulation of JDK internals) is narrower than 8→11.
Prompt 11→17-1 — POJO → Record
What it does: Identifies Java classes in a file that are pure data carriers (final fields, constructor, getters, equals/hashCode/toString) and converts them to Java 17 records. Flags any class that cannot be converted — inheritance, mutable fields, JPA @Entity — and explains why.
When to use it: On DTO, request/response, value-object, and event classes. Skip @Entity classes — JPA requires mutability.
Identify Java classes in this file that are pure data carriers (final fields, constructor, getters, equals/hashCode, toString). Convert each to a Java 17 record. Preserve all constructor validation in a compact constructor. Flag any class that cannot be converted and explain the reason.
Prompt 11→17-2 — Pattern Matching for instanceof
What it does: Replaces every if (x instanceof Type) { Type t = (Type) x; ... } pattern with Java 17 pattern matching: if (x instanceof Type t) { ... }. Removes the redundant cast and intermediate variable in one step.
When to use it: Files with long if-instanceof chains or visitor-style dispatch code.
Refactor every 'if (x instanceof Type) { Type t = (Type) x; ... }' in this file to Java 17 pattern matching: 'if (x instanceof Type t) { ... }'. Remove the redundant cast and intermediate variable from each occurrence.
Prompt 11→17-3 — Sealed Hierarchies
What it does: Takes an abstract class or interface with a known finite set of subclasses, makes it sealed with an explicit permits list, and updates any downstream switch statements to be exhaustive — which lets the compiler catch missing cases.
When to use it: Domain model hierarchies where you know all subtypes at compile time (e.g. Shape, PaymentMethod, Event types).
Given this abstract class or interface with a known finite set of subclasses, make it sealed and list all permitted subclasses. Update any downstream switch statements to be exhaustive against the sealed hierarchy and remove default cases where the compiler can now verify completeness.
Prompt 11→17-4 — Text Blocks
What it does: Finds multi-line String concatenations (JSON, SQL, HTML, XML templates built with + or StringBuilder) and converts them to Java 15+ text blocks ("""..."""`). Handles indentation stripping correctly so the output matches the original.
When to use it: Any file that builds multi-line strings by concatenation — extremely common in test fixtures, SQL strings, and REST client tests.
Find multi-line String concatenations in this file and convert them to Java 17 text blocks ("""..."""). Preserve exact indentation in the output. Show each before/after and note any cases where the content contains backslash sequences that need special handling inside a text block.
Prompt 11→17-5 — Switch Expressions
What it does: Converts traditional switch statements to Java 14+ switch expressions where the switch produces a value. Uses arrow syntax (case X -> ...) to eliminate fall-through bugs and removes redundant break statements.
When to use it: Any file with switch statements that assign to a variable — the classic sign that a switch expression would be cleaner.
Convert traditional switch statements in this file to Java 17 switch expressions where the switch produces a value. Use arrow syntax (case X -> ...) and remove fall-through. For each conversion, verify the exhaustiveness and add a default only where the compiler requires it.
Java 17 → Java 21
Java 21 is the virtual-threads release. Most upgrades are mechanical — the breakage surface is small, but the opportunity to modernise concurrency is large. The key addition is pattern matching for switch with record deconstruction.
Prompt 17→21-1 — Pattern Matching for switch with Records
What it does: Upgrades a switch statement to use Java 21 pattern matching, including record deconstruction patterns (case Circle(double r)). Makes the switch exhaustive against a sealed hierarchy where one exists. Goes beyond what Prompt 11→17-2 did for plain instanceof.
When to use it: After converting your hierarchy to sealed + records (steps above). This unlocks the most expressive form of pattern matching.
Refactor this switch statement to use Java 21 pattern matching for switch, including record deconstruction patterns. Make the switch exhaustive against the sealed hierarchy if one exists. Add guard clauses (when) where appropriate to refine matches.
Prompt 17→21-2 — Virtual Thread Migration Audit
What it does: Audits a class for executor usage and proposes migrations to Executors.newVirtualThreadPerTaskExecutor() for I/O-bound workloads. Flags synchronized blocks around blocking calls (carrier-pinning risk on Java 21–24) and explains the risk for each.
When to use it: The first step in any virtual-thread migration — always audit before changing code. See also: Virtual Threads benchmark.
Audit this class for executor usage. For each I/O-bound workload, propose a migration to Executors.newVirtualThreadPerTaskExecutor(). Flag any synchronized blocks that contain blocking calls (carrier pinning risk on Java 21-24) and suggest ReentrantLock replacements.
Prompt 17→21-3 — Sequenced Collections
What it does: Replaces verbose first/last access patterns with the new Java 21 sequenced collection API. Mechanically safe — behaviour is identical after the change.
When to use it: Quick modernization pass on any Java 21+ codebase. High readability gain, zero risk.
Replace list.get(0) with list.getFirst(), list.get(list.size()-1) with list.getLast(), and Collections.reverse(list) with list.reversed() where applicable in this file. Show each replacement and confirm the return type of reversed() is a view (not a copy) where relevant.
Prompt 17→21-4 — ThreadLocal Identification
What it does: Identifies all ThreadLocal usages in a file and evaluates each one: is it request-scoped (migrate to ScopedValue), connection-scoped (keep), or a static cache (evaluate separately)? Produces a migration plan with before/after code for each request-scoped usage.
When to use it: Before flipping to virtual threads — fat ThreadLocal values are the most common source of memory pressure at scale.
Identify ThreadLocal usages in this file and classify each: request-scoped data (candidate for ScopedValue), connection-scoped data (keep), or static cache (evaluate separately). For each request-scoped ThreadLocal, show a before/after migration to ScopedValue with an explanation.
Prompt 17→21-5 — StructuredTaskScope Trial
What it does: Converts a CompletableFuture.allOf fan-out to StructuredTaskScope (preview in Java 21, final in Java 25). Explains the key difference: StructuredTaskScope automatically cancels all forked tasks when the scope exits, which allOf does not.
When to use it: Fan-out patterns in aggregators or BFF (backend-for-frontend) services that combine results from multiple microservice calls.
Refactor this CompletableFuture.allOf fan-out to StructuredTaskScope (Java 21 preview / Java 25 final). Show both ShutdownOnFailure and ShutdownOnSuccess variants. Explain how automatic cancellation of forked tasks differs from the CompletableFuture.allOf behaviour.
Java 21 → Java 25
The smoothest LTS jump in years — mostly finalizing preview features and removing long-deprecated APIs. The main work is removing --enable-preview flags and replacing a small set of deleted APIs.
Prompt 21→25-1 — Finalize Preview Features
What it does: Removes all --enable-preview flags from a build file and confirms that every preview API used (StructuredTaskScope, ScopedValue, unnamed patterns) has a final equivalent in Java 25. Updates import statements if package names changed between preview and final.
When to use it: The very first step of the 21→25 upgrade — clean up the build before touching any source code.
Remove all --enable-preview flags from this build file. For each preview API used in the project (StructuredTaskScope, ScopedValue, unnamed patterns), confirm it has a final equivalent in Java 25 and update any import statements that changed between preview and GA.
Prompt 21→25-2 — Remove SecurityManager
What it does: Finds every System.setSecurityManager() call and SecurityManager subclass (removed in Java 25 — now throws UnsupportedOperationException) and suggests OS-level sandboxing or Java agent alternatives for each use case.
When to use it: When the compiler or runtime reports SecurityManager-related errors after moving to Java 25.
Find every System.setSecurityManager call and SecurityManager subclass in this codebase. For each, propose a Java 25 alternative: OS-level sandboxing, Java agent, or AccessController removal where it was used only for auditing.
Prompt 21→25-3 — Finalizers → Cleaner
What it does: Replaces Object.finalize() overrides (removed by default in Java 25) with java.lang.ref.Cleaner or try-with-resources. Shows the registration pattern and explains why Cleaner is more reliable than finalization.
When to use it: Any class that overrides finalize() for resource cleanup — common in legacy JDBC wrappers, native resource holders, and old cache implementations.
Replace every Object.finalize() override in this file with java.lang.ref.Cleaner or try-with-resources (AutoCloseable). Show the Cleaner registration pattern and explain why it is more reliable than finalization for native resource cleanup.
Prompt 21→25-4 — Unsafe Memory → MemorySegment
What it does: Replaces sun.misc.Unsafe memory operations (allocation, get/put at raw addresses) with java.lang.foreign.MemorySegment (final in Java 22+, JEP 454). Keeps semantics identical while using safe, bounds-checked off-heap memory.
When to use it: Low-level libraries, serialization frameworks, or native interop code that accesses off-heap memory through Unsafe.
Replace sun.misc.Unsafe memory operations in this file with java.lang.foreign.MemorySegment (final in Java 22+). Keep the semantics identical. Show the Arena allocation pattern for managing segment lifetimes, and explain how bounds-checking in MemorySegment prevents the class of bugs that Unsafe allowed.
Prompt 21→25-5 — StructuredTaskScope Adoption
What it does: Refactors CompletableFuture.allOf fan-outs to Java 25 final StructuredTaskScope. Covers both failure-propagation joiners and success-on-first joiners. Explains how automatic cancellation and structured error propagation differ from the CompletableFuture approach.
When to use it: The final modernization step — once the project is running on Java 25, replace the remaining CompletableFuture fan-outs with StructuredTaskScope for proper lifecycle management.
Refactor this CompletableFuture.allOf fan-out to Java 25 StructuredTaskScope (final API). Explain error propagation and cancellation differences. Show both ShutdownOnFailure (cancel all if any fails) and anySuccessfulResultOrThrow (cancel all once first succeeds) variants.
How to Use This Playbook
- Pick your current stage. Start from the prompt set closest to your current Java version. Do not skip stages — jumping 8→25 in one shot compounds the breakage surface and makes problems harder to isolate.
- Run prompts one file at a time. Attach the file, run the prompt, review the diff, commit. A 2,000-line God class is too much context for a single prompt — split it first.
- Accept diffs in a feature branch per stage. Keep each LTS jump in its own Git branch so you have a clean rollback point.
- Run your full test suite between stages. Every stage should leave CI green before you proceed. A red build is a blocker, not a warning.
- Tag a release after each successful stage. A tagged commit gives your team a clear rollback target if something surfaces in QA later.
- Only move to the next stage after a full working day of green CI. Integration issues often surface hours after a commit, not immediately.
See Also
- Java 21 to Java 25 LTS: Every Feature You Actually Need to Know
- Virtual Threads vs Platform Threads — Benchmarks and Code
- Java Concurrency Deep Dive: CompletableFuture, ExecutorService, ForkJoinPool
- Java Streams API Deep Dive + Collectors Cookbook
- Modern Java Testing: JUnit 6 + AssertJ + Mockito 5 + Testcontainers
- Java 21 to Java 25 Upgrade Guide (with AI Prompts)
- 10 AI Prompts to Generate JUnit 6 Tests for New Projects
- 10 AI Prompts to Debug and Fix JUnit 6 Test Failures
- Overview of New Features in Java 19
- Java Memory Management and Garbage Collection: Deep Dive
FAQs
Which LLM works best for these prompts?
Any frontier model — Claude (Sonnet or Opus), GPT-4-class, Gemini 1.5 Pro — handles these prompts well. Long-context models (200k+ token windows) are better for multi-file refactors where you need to pass an entire module at once. For incremental file-by-file work, a fast Sonnet-class model is the most cost-efficient choice.
Should I upgrade to a non-LTS version on the way?
Only for experimentation in a development environment. Production deployments should hop LTS to LTS: 8 → 11 → 17 → 21 → 25. Non-LTS versions (12–16, 18–20, 22–24) receive only six months of support and are not covered by most enterprise support contracts.
Do I need to memorize the prompts?
No — bookmark this page and copy-paste. Treat each prompt as a template: the bracketed placeholders ([paste your algorithm], [describe what you want]) are the only parts you customise. The framing, constraints, and output format are already tuned for the task.
What if the AI-generated diff breaks something?
Review every diff before committing — treat the AI output as a pull request from a capable but fallible collaborator. The test suite is your safety net. If a prompt produces code that breaks tests, paste the failing test + the generated code back into the AI and ask it to fix the regression. The iterative review loop is the real workflow; the prompts are the starting point.
Conclusion
Java version upgrades used to be week-long archaeology projects. With the prompts above and a modern LLM, a typical module upgrade is measured in hours. The 20 prompts in this playbook cover every high-effort, repetitive task across all four LTS jumps. Save this post, bookmark the stage you are currently tackling, and reach for each prompt when the diffs start looking repetitive — that is exactly the kind of work LLMs are best at.