# Modern Java

> Implements modern Java language features (JDK 21–26) including virtual threads, pattern matching switch expressions, records, sequenced collections, and scoped values for production application development.

- Skill: `paulpas/modern-java` (Agent Skill)
- Install (CLI): `npx skillmds@latest add paulpas/modern-java`
- Raw SKILL.md: https://api.skillmd.com/api/skills/paulpas/modern-java/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: paulpas (https://skillmd.com/u/paulpas)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/paulpas/modern-java

---






# Modern Java Development (JDK 21–26)

Implements idiomatic modern Java code using language features from JDK 21 through JDK 26 — virtual threads for concurrency, pattern matching in switch and instanceof, records with compact constructors, sequenced collections, scoped values for immutable data sharing, and the Foreign Function & Memory API for safe native interop. The model produces typed, production-ready code following Java best practices for the current LTS (JDK 21) and stable non-LTS releases.

## TL;DR Checklist

- [ ] Use `Executors.newVirtualThreadPerTaskExecutor()` for I/O-bound workloads — never use platform thread pools for this
- [ ] Apply pattern matching with `instanceof Type variable` instead of raw casts after instanceof checks
- [ ] Use switch expressions with pattern matching (`case Type var ->`) for type-based dispatch on sealed hierarchies
- [ ] Declare records for immutable data carriers; include compact constructors when validation logic is needed
- [ ] Prefer `SequencedCollection` / `SequencedMap` interfaces over plain `List` or `Map` when first/last operations are needed
- [ ] Use `ScopedValue` (JDK 25+) instead of `ThreadLocal` for immutable context passed across virtual threads
- [ ] Never share mutable state between virtual threads without explicit synchronization

---

## When to Use

Use this skill when:

- Writing new Java code on JDK 21+ and wanting to use modern language features
- Migrating legacy thread-per-request server code to virtual threads for throughput improvement
- Refactoring instanceof chains with manual casts into pattern matching expressions
- Replacing verbose POJOs with immutable `record` types for DTOs, events, or configuration
- Working on high-concurrency applications where traditional thread pools exhaust resources
- Building native interop code and considering the Foreign Function & Memory API over JNI

---

## When NOT to Use

Avoid this skill for:

- Targeting JDK 17 or earlier — features like virtual threads, records (pre-JDK 16 preview), and pattern matching switch require JDK 21+
- Framework-specific development concerns (Spring Boot configuration, Quarkus build config) — those are framework skills
- CPU-bound computational workloads — use `ForkJoinPool` or bounded platform thread pools instead of virtual threads
- Projects requiring strict backward compatibility with JDK 8 or 11 codebases

---

## Core Workflow

### Step 1: Assess Workload Type — I/O-Bound vs CPU-Bound

Determine whether the task is I/O-bound (waiting on external systems) or CPU-bound (computation). This decision dictates concurrency strategy.

- **I/O-bound**: Database queries, HTTP client calls, file reads/writes, message queue consumption → use virtual threads
- **CPU-bound**: Sorting large collections, cryptographic hashing, image processing, JSON deserialization of millions of records → use `ForkJoinPool` or bounded platform-thread `FixedThreadPool`

**Checkpoint:** If the code spends significant time blocked on I/O (network latency, disk reads, database round-trips), virtual threads are appropriate. CPU-bound tasks will not benefit from virtual threads and may suffer overhead.

### Step 2: Choose Immutable Data Structures

Replace mutable POJOs with records for data carriers, DTOs, configuration objects, and domain events. Use compact constructors only when validation logic is required. Apply `SequencedCollection` / `SequencedMap` interfaces when first/last operations are needed.

**Checkpoint:** Verify that all components declared in the record are effectively final — no mutation after construction. If mutation is needed, use a regular class.

### Step 3: Apply Pattern Matching for Dispatch

Replace instanceof chains with manual casts using pattern matching for `instanceof` in `if` guards, and switch expressions with pattern matching on sealed hierarchies. This eliminates runtime `ClassCastException`, removes boilerplate casts, and enables compiler-enforced exhaustiveness checking.

**Checkpoint:** If the switch covers all members of a sealed interface, omit the `default` case — the compiler will flag missing cases at compile time.

### Step 4: Structure Concurrency for Parallel Tasks

Group related tasks using `StructuredTaskScope` (JDK 21+) when multiple parallel sub-tasks must complete together. If one task fails, all siblings are shut down and the scope throws an exception — eliminating orphan threads.

**Checkpoint:** Always close structured concurrency scopes via try-with-resources (`try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { ... }`) to ensure proper cancellation of forked tasks.

### Step 5: Replace ThreadLocal with Scoped Values

For JDK 25+, migrate `ThreadLocal` usage to `ScopedValue` for immutable context data shared across threads, especially when using virtual threads. Virtual thread multiplexing breaks `ThreadLocal` isolation — scoped values provide safe per-thread access without platform thread pinning.

**Checkpoint:** Verify that all code paths within a scoped value's `bind()` lambda complete before the scope exits, and that no async callbacks escape the bound region.

---

## Implementation Patterns

### Pattern 1: Virtual Thread Executor (BAD vs GOOD)

Replace platform-thread executors with virtual thread executors for I/O-bound workloads. Virtual threads are lightweight JVM-managed threads that multiplex onto a smaller number of carrier (platform) threads, enabling millions of concurrent tasks without exhausting system resources.

```java
// ❌ BAD — Platform thread executor leaks connections under high I/O concurrency
public void processOrdersBad(List<Order> orders) throws InterruptedException {
    ExecutorService executor = Executors.newFixedThreadPool(200);

    List<CompletableFuture<OrderResult>> futures = orders.stream()
        .map(order -> CompletableFuture.supplyAsync(
            () -> processSingleOrder(order), executor))
        .toList();

    // Platform threads block on I/O — pool exhausts and tasks queue up
    for (CompletableFuture<OrderResult> f : futures) {
        OrderResult result = f.get(30, TimeUnit.SECONDS);
        System.out.println("Processed order " + result.orderId());
    }

    executor.shutdown();
}

// ✅ GOOD — Virtual thread executor with try-with-resources auto-close
public void processOrdersGood(List<Order> orders) {
    // Each task gets a lightweight virtual thread; JVM creates ~60K+ per GB of heap
    try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {

        List<OrderResult> results = orders.stream()
            .map(order -> executor.submit(() -> processSingleOrder(order)).join())
            .toList();

        results.forEach(r -> System.out.println("Processed order " + r.orderId()));
    } // AutoCloseable — executor joins all virtual threads on close
}

// ✅ GOOD — Bounded concurrency using Semaphore for downstream system protection
public void processOrdersBounded(List<Order> orders) {
    // Limit concurrent I/O to what the database can handle safely
    var semaphore = new Semaphore(50);

    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        List<OrderResult> results = orders.stream()
            .map(order -> executor.submit(() -> {
                try {
                    semaphore.acquire();
                    return processSingleOrder(order);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Task interrupted", e);
                } finally {
                    semaphore.release();
                }
            }))
            .toList()
            .stream()
            .map(CompletableFuture::join)
            .toList();
    }
}
```

### Pattern 2: Pattern Matching Switch on Sealed Hierarchy (BAD vs GOOD)

Sealed classes restrict which types can extend or implement them, creating a closed type hierarchy. This enables exhaustive pattern matching in switch expressions — the compiler verifies all permitted subclasses are handled, eliminating unreachable default cases and missing-case bugs.

```java
// ❌ BAD — Legacy instanceof chain with manual casts, runtime ClassCastException risk
public double calculatePayment(Object obj) {
    if (obj instanceof Invoice) {
        Invoice invoice = (Invoice) obj;
        return invoice.getAmount() * (1 - invoice.getDiscountRate());
    } else if (obj instanceof Payment) {
        Payment payment = (Payment) obj;
        return payment.getPrincipal();
    } else if (obj instanceof Refund) {
        Refund refund = (Refund) obj;
        return refund.getAmount() * -1;
    } else if (obj instanceof String s) {
        return Double.parseDouble(s);
    }
    throw new IllegalArgumentException("Unknown type: " + obj.getClass());
}

// ✅ GOOD — Sealed interface with exhaustive pattern matching switch
public record Invoice(int id, double amount, double discountRate) implements Transaction {}
public record Payment(int id, double principal) implements Transaction {}
public record Refund(int id, double amount) implements Transaction {}

public sealed interface Transaction permits Invoice, Payment, Refund {}

// Switch expression with pattern matching — compiler enforces exhaustiveness
public double calculatePayment(Transaction tx) {
    return switch (tx) {
        case Invoice invoice -> invoice.amount() * (1 - invoice.discountRate());
        case Payment payment -> payment.principal();
        case Refund refund   -> refund.amount() * -1;
        // No default needed — compiler guarantees all permits are covered
    };
}

// ✅ GOOD — instanceof pattern matching in if guards with deconstruction
public record Point(int x, int y) {}
public record Circle(Point center, double radius) extends Shape {}
public record Rectangle(Point tl, Point br) extends Shape {}
public abstract sealed class Shape permits Circle, Rectangle {}

public String describeShape(Shape shape) {
    // Pattern matching instanceof with type pattern variable
    if (shape instanceof Circle c && c.radius() > 0) {
        return "Circle at (%d,%d) with radius %.2f".formatted(c.center.x(), c.center.y(), c.radius());
    }
    if (shape instanceof Rectangle r) {
        var width = Math.abs(r.br().x() - r.tl().x());
        var height = Math.abs(r.br().y() - r.tl().y());
        return "Rectangle %dx%d".formatted(width, height);
    }
    throw new IllegalArgumentException("Unhandled shape: " + shape.getClass().getSimpleName());
}
```

### Pattern 3: Records with Compact Constructors and Computed Components

Records provide immutable value objects with auto-generated `equals()`, `hashCode()`, and `toString()`. Use compact constructors for validation logic, and add computed methods for derived behavior. Records are ideal for DTOs, domain events, configuration values, and API response types.

```java
// ✅ GOOD — Record for simple data carrier with no validation needed
public record OrderId(long value) {
    public OrderId {
        if (value <= 0) throw new IllegalArgumentException("Order ID must be positive");
    }
}

// ✅ GOOD — Generic record for paginated API responses with nested metadata
public record PagedResponse<T>(List<T> items, PageMetadata pageInfo) {
    public static <T> PagedResponse<T> empty(int page, int pageSize, long totalElements) {
        return new PagedResponse<>(List.of(), new PageMetadata(page, pageSize, totalElements));
    }

    public boolean hasNextPage() {
        return (long) (pageInfo.page + 1) * pageInfo.size < pageInfo.total;
    }

    public record PageMetadata(int page, int size, long total) {
        public int totalPages() {
            return (int) Math.ceil((double) total / size);
        }
    }
}

// ✅ GOOD — Record with compact constructor for multi-field validation
public record UserRegistration(String email, String displayName, LocalDateTime createdAt) {
    public UserRegistration(String email, String displayName) {
        this(email, displayName, LocalDateTime.now());
        // Validate email format using regex
        if (!email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")) {
            throw new IllegalArgumentException("Invalid email: " + email);
        }
        // Validate display name is not empty or whitespace-only
        if (displayName == null || displayName.isBlank()) {
            throw new IllegalArgumentException("Display name is required");
        }
        if (displayName.length() > 50) {
            throw new IllegalArgumentException("Display name exceeds 50 characters");
        }
    }
}

// ❌ BAD — Do NOT use records for objects that need mutability
public class MutableConfig {
    private String apiKey;
    public void setApiKey(String key) { this.apiKey = key; } // Records are immutable
}
// Use a regular class when mutation is required.
```

### Pattern 4: Sequenced Collections (JDK 21+)

Sequenced Collections (JEP 448) add `addFirst`, `addLast`, `getFirst`, `getLast`, and `reverse()` to the collection hierarchy. Use `SequencedCollection` when you need deque-like operations, or `SequencedMap` for first/last key access instead of relying on implementation-specific behavior.

```java
// ❌ BAD — Using plain List with O(n) first element access
public class BadOrderProcessor {
    public String getLastProcessedId(List<String> orderIds) {
        if (orderIds.isEmpty()) return null;
        // Must traverse entire list — O(n) even for last element
        return orderIds.get(orderIds.size() - 1);
    }

    public void addOrder(List<String> queue, String orderId) {
        // No standard List method for adding to front — confusing intent
        queue.add(0, orderId);  // This shifts all elements — O(n)
    }
}

// ✅ GOOD — SequencedCollection provides explicit first/last operations
public class OrderProcessor {

    // Return type exposes the API contract: caller needs first/last semantics
    private final Deque<String> orderQueue = new ArrayDeque<>();

    public String getLastProcessedId() {
        return orderQueue.getLast(); // O(1) — declared on SequencedCollection
    }

    public void addOrder(String orderId) {
        orderQueue.addLast(orderId); // O(1) — explicit intent
    }

    public String nextOrderId() {
        return orderQueue.pollFirst(); // Remove and return first element
    }

    public int queueSize() {
        return orderQueue.size();
    }

    // ✅ GOOD — SequencedMap for first/last key access
    public static void processConfiguration(Map<String, String> config) {
        var sequencedConfig = new LinkedHashMap<String, String>(config);

        // First entry is the base configuration
        String baseEnv = sequencedConfig.firstKey();
        // Last entry overrides everything
        String overrideEnv = sequencedConfig.lastKey();

        System.out.println("Base env: " + baseEnv);
        System.out.println("Override env: " + overrideEnv);

        // Reverse iteration for priority processing (last to first)
        for (var entry : sequencedConfig.reversed()) {
            System.out.println(entry.getKey() + "=" + entry.getValue());
        }
    }
}
```

### Pattern 5: Scoped Values for Immutable Context (JDK 25+)

Scoped Values (JEP 506) provide a lower-overhead alternative to `ThreadLocal` for passing immutable data across threads. Unlike `ThreadLocal`, scoped values are safe with virtual thread multiplexing because the value is bound explicitly within a lambda scope rather than implicitly tied to the carrier thread. This prevents context pollution when virtual threads swap platform carriers.

```java
// ❌ BAD — ThreadLocal breaks with virtual thread multiplexing
public class BadContextHolder {
    // Virtual threads share platform threads → ThreadLocal data leaks across requests
    private static final ThreadLocal<String> USER_ID = ThreadLocal.withInitial(() -> "anonymous");

    public void handleRequest(String userId) {
        USER_ID.set(userId);           // Sets on platform thread, not virtual thread
        // Another virtual thread on same platform thread may see stale data
        processRequest();              // USER_ID.get() could return wrong user
    }
}

// ✅ GOOD — ScopedValue with explicit binding scope (JDK 25+)
public final class RequestContext {
    // Declaring a scoped value — immutable, bound within a lambda
    private static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();
    private static final ScopedValue<RequestId> REQUEST_ID = ScopedValue.newInstance();

    private RequestContext() {}

    /** Execute a request handler with bounded context data.
     *  Only code inside the bind() lambda can access these values. */
    public static void execute(
        String userId,
        RequestId requestId,
        Runnable handler
    ) {
        ScopedValue.where(CURRENT_USER, userId)
                   .where(REQUEST_ID, requestId)
                   .run(handler);
    }

    /** Get current user — only valid within a bound scope */
    public static String getCurrentUser() {
        return CURRENT_USER.get();
    }

    /** Get current request ID — only valid within a bound scope */
    public static RequestId getCurrentRequestId() {
        return REQUEST_ID.get();
    }

    /** Run multiple operations under the same scoped context, in parallel */
    public static <R> List<R> executeParallel(
        String userId,
        RequestId requestId,
        List<java.util.concurrent.Callable<R>> tasks
    ) throws Exception {
        var scope = new jdk.incubator.concurrent.StructuredTaskScope<>();

        return ScopedValue.where(CURRENT_USER, userId)
            .where(REQUEST_ID, requestId)
            .callAll(tasks.stream()
                .map(task -> scope.fork(() -> task.call()))
                .toList()
            ).results().stream()
            .map(jdk.incubator.concurrent.StructuredTaskScope.Subtask::get)
            .toList();
    }

    public record RequestId(long value) {
        public RequestId { if (value <= 0) throw new IllegalArgumentException("ID must be positive"); }
    }
}

// Usage: Pass user context across threads safely with virtual threads
public class OrderService {

    public void placeOrder(String userId, String orderId) {
        var requestId = new RequestContext.RequestId(System.nanoTime());

        // Context is bound only within the lambda — safe even with virtual threads
        RequestContext.execute(userId, requestId, () -> {
            System.out.println("Processing order for: " + RequestContext.getCurrentUser());
            System.out.println("Request ID: " + RequestContext.getCurrentRequestId());
            processPayment(orderId);
            notifyCustomer(orderId);
        });
    }

    private void processPayment(String orderId) { /* ... */ }
    private void notifyCustomer(String orderId) { /* ... */ }
}
```

### Pattern 6: Foreign Function & Memory API (JEP 454)

The Foreign Function & Memory API provides a safe replacement for JNI in Java. Use `MemorySegment` for native memory, `Arena` for scoped allocation, and `MemoryLayout` for defining C-compatible struct layouts. This approach avoids the complexity and security risks of JNI while providing near-zero overhead interop with C libraries.

```java
// ✅ GOOD — Reading a file into native memory using Foreign Memory API (JDK 22+)
import java.lang.foreign.*;
import java.nio.channels.FileChannel;
import java.io.IOException;
import java.nio.file.Path;

public class NativeFileReader {

    private static final Arena ARENA = Arena.ofConfined();

    /** Read a file into a native memory segment and return bytes.
     *  Uses MemorySegment.mapToMemory for zero-copy access on supported platforms. */
    public byte[] readFile(Path path) throws IOException {
        try (var channel = FileChannel.open(path)) {
            long fileSize = channel.size();

            // Map the file directly into native memory — no intermediate heap allocation
            var segment = channel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize, ARENA);

            // Convert to Java heap bytes if needed for processing
            byte[] heapCopy = new byte[(int) fileSize];
            MemoryAccess.getArrayBaseOffset(byte[].class);
            segment.copyTo(0, heapCopy, 0, fileSize);

            return heapCopy;
        }
    }

    // ✅ GOOD — Define a C struct layout and read values from native memory
    public static record Point(int x, int y) {}

    public static final MemoryLayout POINT_LAYOUT = MemoryLayout.ofStruct(
        MemoryLayout.ofValueBits(32, ByteOrder.BIG_ENDIAN),  // int x
        MemoryLayout.ofValueBits(32, ByteOrder.BIG_ENDIAN)   // int y
    );

    /** Read a Point struct from native memory at the given address offset */
    public static Point readPoint(MemorySegment segment, long offset) {
        var access = segment.asSlice(offset, POINT_LAYOUT.byteSize());
        int x = MemoryAccess.getIntUnaligned(access, MemoryLayout.PathElement.groupElement("x"));
        int y = MemoryAccess.getIntUnaligned(access, MemoryLayout.PathElement.groupElement("y"));
        return new Point(x, y);
    }
}

// ❌ BAD — Traditional JNI approach with native library coupling
public class BadNativeAccess {
    // Requires a compiled .so/.dll file maintained separately from Java code
    static { System.loadLibrary("mylib"); }
    public native int compute(int a, int b);  // No type safety, no compilation checking
}
// JNI requires maintaining native C/C++ code, separate build pipelines, and
// carries significant security risks from buffer overflows and memory corruption.
```

---

## Constraints

### MUST DO

- Use `Executors.newVirtualThreadPerTaskExecutor()` for all I/O-bound task execution — never use platform thread pools for this purpose
- Declare sealed interfaces/classes with an explicit `permits` clause listing every permitted subclass to enable exhaustive pattern matching
- Apply pattern matching with instanceof (`if (obj instanceof SomeType variable)`) instead of raw casts after instanceof checks
- Use records for immutable data carriers and DTOs; include compact constructors when validation logic is required
- Always close virtual thread executors via try-with-resources (`try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { ... }`)
- Prefer `SequencedCollection` / `SequencedMap` over plain `List` / `Map` when first/last operations are part of the API contract
- Use `ScopedValue` (JDK 25+) instead of `ThreadLocal` for immutable context shared across threads, especially with virtual threads
- Set explicit timeouts on all blocking I/O calls — `HttpClient` with `.connectTimeout()` and JDBC statements with `.setQueryTimeout()`

### MUST NOT DO

- Use virtual threads for CPU-bound computation — use `ForkJoinPool` or bounded platform thread pools instead
- Share mutable state between virtual threads without explicit synchronization — virtual thread scheduling is non-deterministic
- Leave a `StructuredTaskScope` unclosed via try-with-resources — this causes resource leaks and prevents proper task cancellation
- Use raw `instanceof` checks with manual casting (`if (obj instanceof String) { String s = (String) obj; }`) when pattern matching is available
- Use records for objects that require mutability — they are fundamentally immutable by design
- Rely on `ThreadLocal` for per-thread state in virtual thread applications — the multiplexing model breaks isolation guarantees

---

## Live References

> Authoritative documentation links for modern Java language features (JDK 21–26). The model follows markdown links at load time to resolve external references and inline content.

- [OpenJDK Project Loom — Virtual Threads](https://openjdk.org/projects/loom/)
- [Java Language Guide — Records](https://docs.oracle.com/en/java/javase/21/language/records.html)
- [Sequenced Collections (JEP 448)](https://openjdk.org/jeps/448)
- [Scoped Values (JEP 506, JDK 25 Preview)](https://openjdk.org/jeps/506)
- [Foreign Function & Memory API (JEP 454)](https://openjdk.org/jeps/454)
- [Java SE 21 Language Features Overview](https://docs.oracle.com/en/java/javase/21/language/)
- [OpenJDK JEP Index](https://openjdk.org/jeps/)

---

## Related Skills

| Skill | Purpose |
|---|---|
| `go-concurrency-patterns` | Compare Go's goroutine model with Java's virtual threads — useful when evaluating concurrency approaches across languages |
| `coding-code-review` | General code review methodology — applies when reviewing modern Java code for correctness, security, and adherence to these patterns |

