Java 21 Features skill for Codex
Records, sealed switches and virtual threads done right. Codex checks the project's release level before using any feature, then converts DTOs to records, closed hierarchies to sealed interfaces with default-free pattern switches, and blocking thread pools to virtual threads where the workload fits. It watches for synchronized pinning on JDK 21 to 23 with JFR, keeps preview features out unless the build enables them, and upgrades in small reviewable steps.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- java-21-features Installs to ~/.agents/skills/java-21-features/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install java-21-features The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project java-21-features Installs to .agents/skills/java-21-features/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/java-21-features.zipunzip -o java-21-features.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) java-21-features PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $java-21-features, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Modernizes Java code to Java 21 LTS and later idioms - records for data carriers, sealed interfaces with exhaustive pattern-matching switch, record patterns, virtual threads with Executors.newVirtualThreadPerTaskExecutor, sequenced collections - and knows what Java 22 to 25 finalized (unnamed variables, stream gatherers, flexible constructor bodies, scoped values) plus virtual thread pinning rules. Use when upgrading from Java 8, 11 or 17, replacing Lombok value classes or visitor hierarchies, moving a thread-pool service to virtual threads, or reviewing code for idiomatic modern Java.
Use it when
- A Java codebase is moving from 8, 11 or 17 to 21 or 25
- You are replacing Lombok value classes or instanceof chains with modern constructs
- A blocking I/O service could use virtual threads instead of a large thread pool
- Code review needs to judge whether Java code is idiomatic for its JDK level
Use something else when
- The problem is the Maven or Gradle build itself: use maven-gradle-builds
- You are tuning heap, GC or JIT flags: use jvm-tuning
How to check the result
The build passes at the agreed release level, a temporary extra subtype breaks every sealed switch at compile time, and JFR shows no long jdk.VirtualThreadPinned events under load.
The mistake it prevents
Adding a default branch to a switch over a sealed type, which turns the compile-time exhaustiveness check into a silent runtime fallthrough when a new subtype appears.
Done well means: the code uses the newest feature the project’s release level allows
(never a preview feature without --enable-preview agreed by the team), switches over
sealed types compile without a default so new subtypes break the build, and virtual
threads replace pools only for blocking I/O work, with pinning and ThreadLocal use checked.
Establish the language level first
java -version
grep -n 'maven.compiler.release\|<release>\|languageVersion\|sourceCompatibility\|jvmTarget' pom.xml build.gradle* 2>/dev/null
| Build says | You may use |
|---|---|
release 17 | records, sealed, instanceof patterns, text blocks, switch expressions |
release 21 | + pattern matching for switch, record patterns, virtual threads, sequenced collections |
release 22+ | + unnamed variables and patterns _ (final in 22), FFM API |
release 24+ | + stream gatherers; synchronized no longer pins virtual threads (JEP 491) |
release 25 (LTS) | + flexible constructor bodies, module import declarations, compact source files with instance main, scoped values |
Follow AGENTS.md for the target. Raising the release level is a deployment decision
(runtime images, CI JDK, library consumers); ask before changing it. Structured concurrency
and primitive patterns are still preview in 25; do not use them in production code unless
the build already enables preview.
Records
public record Money(BigDecimal amount, Currency currency) {
public Money { // compact canonical constructor
Objects.requireNonNull(amount, "amount");
Objects.requireNonNull(currency, "currency");
if (amount.scale() > currency.getDefaultFractionDigits())
throw new IllegalArgumentException("too many decimals: " + amount);
}
public Money plus(Money o) {
if (!currency.equals(o.currency)) throw new IllegalArgumentException("currency mismatch");
return new Money(amount.add(o.amount), currency);
}
}
public record Order(String id, List<Line> lines) {
public Order { lines = List.copyOf(lines); } // defensive copy: records are shallowly immutable
}
Use a record for DTOs, value objects, map keys, multiple return values and events. Do not
use one for JPA entities (need no-arg constructor and mutable state; use records for
projections instead), or when you need inheritance. Jackson 2.12+ and most frameworks
handle records natively; drop Lombok @Value in favour of them.
Sealed hierarchies and pattern matching
public sealed interface PaymentResult permits Approved, Declined, NeedsAction {}
public record Approved(String authCode) implements PaymentResult {}
public record Declined(String reason, boolean retryable) implements PaymentResult {}
public record NeedsAction(URI redirect) implements PaymentResult {}
String describe(PaymentResult r) {
return switch (r) { // exhaustive: no default
case Approved(var code) -> "approved " + code;
case Declined(var reason, var retry) when retry -> "retry later: " + reason;
case Declined(var reason, var retry) -> "declined: " + reason;
case NeedsAction(var uri) -> "redirect to " + uri;
};
}
Rules:
- Omit
defaulton switches over sealed types and enums. Withdefault, adding a subtype compiles silently and falls into the wrong branch. - Guarded cases (
when) must come before the unguarded case for the same type. case null ->is allowed in Java 21 switches; without it a null selector throws NPE as before.- Java 22+: replace unused bindings with
_(case Declined(var reason, _) ->). - Replace visitor-pattern boilerplate and
instanceofchains with this form when the hierarchy is closed and owned by you.
Virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<Quote>> futures = suppliers.stream()
.map(s -> executor.submit(() -> client.quote(s))) // blocking HTTP call is fine
.toList();
for (var f : futures) quotes.add(f.get());
} // close() waits for all tasks
Spring Boot 3.2+: spring.threads.virtual.enabled=true runs request handling and
@Async on virtual threads.
| Workload | Virtual threads? |
|---|---|
| Many concurrent blocking I/O calls (HTTP, JDBC, files) | Yes; this is the target |
| CPU-bound computation | No; use a bounded platform pool or parallel streams |
| Need to limit concurrency (DB pool of 20, rate-limited API) | Yes, but guard with a Semaphore; never pool virtual threads |
Heavy per-thread caches in ThreadLocal | Rework first; millions of threads multiply the memory |
| Reactive code already non-blocking | Usually no gain; migrate only to simplify |
Pinning (the carrier thread cannot be released while a virtual thread blocks):
- JDK 21 to 23: blocking inside
synchronizedpins. Replace long blocking sections insidesynchronizedwithReentrantLock, or upgrade the runtime to JDK 24+, where JEP 491 removes this case. Native frames and class initializers still pin on all versions. - Detect with JFR:
java -XX:StartFlightRecording=filename=rec.jfr ...thenjfr print --events jdk.VirtualThreadPinned rec.jfr. On 21 to 23,-Djdk.tracePinnedThreads=fullalso prints stacks (removed in 24). - JDBC drivers and connection pools need versions that avoid
synchronizedaround I/O on JDK 21; check driver release notes before switching.
Sequenced collections and small API wins (Java 21)
list.getFirst(); list.getLast(); list.reversed(); // List, Deque, LinkedHashSet, LinkedHashMap
linkedHashMap.firstEntry(); linkedHashMap.pollLastEntry();
Math.clamp(value, 0, 100);
new StringBuilder().repeat("-", 40);
Replace list.get(list.size() - 1) and new ArrayList<>(list) + Collections.reverse with
these. Also standard in 17+ and worth using: Stream.toList() (unmodifiable result),
text blocks for SQL/JSON, switch expressions, instanceof Foo f patterns, helpful NPE
messages.
Java 22 to 25 highlights worth adopting when the release allows
- Stream gatherers (24):
stream.gather(Gatherers.windowFixed(100))for batching,Gatherers.mapConcurrent(16, fn)for bounded concurrent mapping on virtual threads. - Flexible constructor bodies (25): validate or compute arguments before
super(...). - Scoped values (25):
ScopedValue.where(USER, u).run(task)as an immutable, bounded alternative toThreadLocalfor request context. - Markdown doc comments (23):
///comments render Markdown in javadoc. - Compact source files (25):
void main() { IO.println("hi"); }for scripts and teaching; not for application code.
Upgrade workflow
- [ ] 1. Confirm target JDK with the user; update toolchain in Maven/Gradle and CI image
- [ ] 2. Build with the new JDK at the OLD release level first; fix removed APIs and warnings
- [ ] 3. Upgrade build plugins and libraries known to break (Lombok, Mockito/ByteBuddy, Jacoco, ASM, Spring)
- [ ] 4. Raise release level; build and run the full test suite
- [ ] 5. Modernise in small PRs: records, sealed+switch, sequenced collections, virtual threads
- [ ] 6. For virtual threads: load test, record JFR pinning events, compare p99 latency
OpenRewrite automates many steps (org.openrewrite.java.migrate.UpgradeToJava21 recipe);
it needs the plugin download (network) and its diff must be reviewed like any other change.
Gotchas
- Record accessors are
amount(), notgetAmount(). Frameworks or templates expecting JavaBean getters (older JSP/EL, some mappers) need configuration or adapters. equalson records compares arrays by reference; avoid array components or override.- A
switchstatement (not expression) over a sealed type with patterns must also be exhaustive in 21; old-styleswitchon enums without patterns is not checked. Stream.toList()returns an unmodifiable list; code that later callsaddbreaks.Collectors.toList()does not promise mutability either.Thread.ofVirtual().start(r)threads are daemon threads; the JVM can exit before they finish if nothing joins them.synchronizedblocks are still correct on virtual threads; the issue is only throughput on JDK 21 to 23.
Verification
./mvnw -q verify # or ./gradlew build
javac --release 21 -Xlint:all -Werror ... # where the build exposes compiler args
- Add a test that constructs an invalid record and expects the exception.
- Add a new permitted subtype temporarily and confirm every switch over the sealed type fails to compile; remove it again.
- For virtual threads, run a load test before and after; check
jdk.VirtualThreadPinnedevents and thread count.
Report template
**Target:** Java 21 (release 21), runtime image eclipse-temurin:21-jre
**Changes:** 14 DTOs -> records; PaymentResult sealed + exhaustive switches (visitor removed)
**Virtual threads:** enabled for HTTP handlers (spring.threads.virtual.enabled); JDBC pool guarded at 30
**Pinning:** 2 synchronized blocks around HTTP calls -> ReentrantLock; JFR shows 0 pinned events > 20ms
**Checks:** ./mvnw verify green; load test p99 412ms -> 268ms at 800 rps What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Java 21 Features"
short_description: "Records, sealed switches and virtual threads done right"
default_prompt: "$java-21-features Upgrade this service from Java 17 to 21 and use the new features where they help" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.