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.

  • Languages
  • $java-21-features
  • 196 lines
  • 2 files
  • ~2,300 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- java-21-features

Installs to ~/.agents/skills/java-21-features/ for every project on this machine.

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

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.

~/.agents/skills/java-21-features/SKILL.md

--- name: java-21-features description: Modernizes Java code to Java 21 LTS and later idioms - records for data carriers, sealed interfaces with exhaustive pattern-matching switch, record… license: MIT ---

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 saysYou may use
release 17records, 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 default on switches over sealed types and enums. With default, 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 instanceof chains 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.

WorkloadVirtual threads?
Many concurrent blocking I/O calls (HTTP, JDBC, files)Yes; this is the target
CPU-bound computationNo; 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 ThreadLocalRework first; millions of threads multiply the memory
Reactive code already non-blockingUsually no gain; migrate only to simplify

Pinning (the carrier thread cannot be released while a virtual thread blocks):

  • JDK 21 to 23: blocking inside synchronized pins. Replace long blocking sections inside synchronized with ReentrantLock, 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 ... then jfr print --events jdk.VirtualThreadPinned rec.jfr. On 21 to 23, -Djdk.tracePinnedThreads=full also prints stacks (removed in 24).
  • JDBC drivers and connection pools need versions that avoid synchronized around 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 to ThreadLocal for 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(), not getAmount(). Frameworks or templates expecting JavaBean getters (older JSP/EL, some mappers) need configuration or adapters.
  • equals on records compares arrays by reference; avoid array components or override.
  • A switch statement (not expression) over a sealed type with patterns must also be exhaustive in 21; old-style switch on enums without patterns is not checked.
  • Stream.toList() returns an unmodifiable list; code that later calls add breaks. 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.
  • synchronized blocks 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.VirtualThreadPinned events 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem