| name | java-modern-patterns |
| description | Use when writing Java 21+ code, refactoring to modern idioms, choosing records vs classes, using virtual threads, or applying pattern matching. Do NOT use for JPA (use jpa-patterns) or SQL optimization (use sql-optimization-patterns). |
| paths | **/*.java, **/build.gradle*, **/pom.xml |
Modern Java 21+ Implementation Patterns
Data-oriented programming, concurrency, and modern API patterns for Java 21 through 25.
Quick Start
CRITICAL Rules
These prevent the most common mistakes when adopting modern Java features.
- NEVER use
synchronized blocks with I/O inside virtual thread tasks โ causes thread pinning (fixed in JDK 24+ via JEP 491, but ReentrantLock remains portable)
- ALWAYS prefer
sealed interface + record over class hierarchies for modeling domain alternatives
- PREFER
ScopedValue over ThreadLocal with virtual threads โ ThreadLocal works but wastes memory at scale
- ALWAYS handle all cases in pattern-matching
switch โ rely on compiler exhaustiveness, avoid default
- NEVER pool virtual threads โ create a new one per task via
Thread.startVirtualThread() or Executors.newVirtualThreadPerTaskExecutor()
- ALWAYS use records for DTOs, API responses, and value objects โ not for JPA entities
- NEVER add mutable state to records โ they are transparent carriers of immutable data
- PREFER
switch expressions over if-else chains when branching on type or value
- PREFER
ReentrantLock over synchronized when code may run on virtual threads (required before JDK 24; recommended after for portability)
- PREFER
Gatherers for custom stream intermediate operations over imperative loops with accumulators
Decision Trees
Modeling Domain Types
Need to represent a fixed set of alternatives?
+-- Yes
| +-- Each alternative carries data? --> sealed interface + record subtypes (ADT)
| +-- Alternatives are just labels? --> enum
+-- No
+-- Pure data carrier, immutable? --> record
+-- Need mutable state or identity? --> class
+-- Need inheritance + encapsulation? --> class (possibly sealed)
Choosing Concurrency Model
What kind of concurrent work?
+-- Independent I/O-bound tasks (HTTP, DB, file)
| +-- Need to fan-out & collect results? --> StructuredTaskScope + virtual threads
| +-- Fire-and-forget? --> Thread.startVirtualThread()
| +-- Spring Boot web handler? --> spring.threads.virtual.enabled=true
+-- CPU-bound parallel computation
| --> Platform threads (ForkJoinPool / parallelStream)
+-- Scheduled/periodic tasks
| --> ScheduledExecutorService with platform threads
+-- Need to pass context across threads?
--> ScopedValue (not ThreadLocal)
Pattern Matching Approach
What are you switching on?
+-- sealed interface / sealed class
| --> Exhaustive type-pattern switch (no default!)
+-- record type (need to extract fields)
| --> Record deconstruction pattern: case Rect(var w, var h)
+-- Guarded conditions on patterns
| --> Pattern guards: case String s when s.length() > 10
+-- Mixed types from external API
| --> Type patterns with default fallback
+-- Don't care about the value/variable
--> Unnamed pattern: case Noise(_)
Stream Operation Choice
What kind of transformation?
+-- Standard map/filter/reduce --> Built-in Stream ops
+-- Fixed-size windows (batching) --> Gatherers.windowFixed(n)
+-- Sliding window analysis --> Gatherers.windowSliding(n)
+-- Stateful one-to-many transform --> Custom Gatherer with state
+-- Scan/running accumulation --> Gatherers.scan(seed, op)
+-- Limit by condition (takeWhile++) --> Custom Gatherer
+-- Simple grouping/collecting --> Collectors (terminal op)
Quick Reference
Algebraic Data Type (ADT) Template
public sealed interface PaymentResult
permits PaymentResult.Success, PaymentResult.Declined, PaymentResult.Error {
record Success(String transactionId, BigDecimal amount) implements PaymentResult {}
record Declined(String reason, String code) implements PaymentResult {}
record Error(Exception cause) implements PaymentResult {}
}
public String describe(PaymentResult result) {
return switch (result) {
case Success(var txId, var amount) -> "Paid %s: %s".formatted(amount, txId);
case Declined(var reason, var _) -> "Declined: " + reason;
case Error(var cause) -> "Error: " + cause.getMessage();
};
}
Virtual Threads + Structured Concurrency Template
public OrderDetails fetchOrderDetails(Long orderId) throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
Subtask<Order> orderTask = scope.fork(() -> orderService.findById(orderId));
Subtask<Customer> custTask = scope.fork(() -> customerService.findByOrderId(orderId));
Subtask<List<Item>> itemsTask = scope.fork(() -> itemService.findByOrderId(orderId));
scope.join();
return new OrderDetails(
orderTask.get(),
custTask.get(),
itemsTask.get()
);
}
}
Scoped Values Template
private static final ScopedValue<UserContext> CURRENT_USER = ScopedValue.newInstance();
public void handleRequest(UserContext user, Runnable handler) {
ScopedValue.where(CURRENT_USER, user).run(handler);
}
public void auditLog(String action) {
UserContext user = CURRENT_USER.get();
logger.info("user={} action={}", user.id(), action);
}
Record Patterns & Guards
sealed interface Shape permits Circle, Rect {}
record Circle(Point center, double radius) implements Shape {}
record Rect(Point topLeft, Point bottomRight) implements Shape {}
record Point(double x, double y) {}
public double area(Shape shape) {
return switch (shape) {
case Circle(_, var r) when r <= 0 -> 0;
case Circle(_, var r) -> Math.PI * r * r;
case Rect(Point(var x1, var y1),
Point(var x2, var y2)) -> Math.abs((x2 - x1) * (y2 - y1));
};
}
Stream Gatherers
List<List<Integer>> batches = IntStream.rangeClosed(1, 100)
.boxed()
.gather(Gatherers.windowFixed(10))
.toList();
record Avg(double sum, int count) {
double value() { return sum / count; }
}
List<Double> runningAvg = prices.stream()
.gather(Gatherers.scan(() -> new Avg(0, 0),
(avg, price) -> new Avg(avg.sum() + price, avg.count() + 1)))
.map(Avg::value)
.toList();
Unnamed Variables & Patterns
try {
return Integer.parseInt(input);
} catch (NumberFormatException _) {
return defaultValue;
}
map.forEach((_, value) -> process(value));
case Success(_, var amount) -> handleAmount(amount);
Workflow Instructions
When designing a domain model:
- Identify the alternatives (sum types) โ use
sealed interface
- Define each variant's data โ use
record for each permitted subtype
- Add behavior via pattern-matching
switch outside the hierarchy
- Leverage exhaustiveness โ let the compiler catch missing cases
- For deep patterns: references/data-oriented-programming.md
When migrating to virtual threads:
- Enable in Spring Boot:
spring.threads.virtual.enabled=true
- Replace
synchronized with ReentrantLock in I/O-critical paths
- Replace
ThreadLocal with ScopedValue
- Detect thread pinning:
-Djdk.tracePinnedThreads=full
- For patterns: references/virtual-threads-concurrency.md
When building concurrent workflows:
- Identify independent I/O tasks that can run in parallel
- Use
StructuredTaskScope.open() to fork subtasks
- Call
scope.join() to wait โ cancellation propagates automatically
- Extract results via
subtask.get()
- For advanced patterns: references/virtual-threads-concurrency.md
When modernizing legacy (Effective Java era) code:
- Identify tagged classes โ convert to
sealed interface + record
- Replace
synchronized + I/O with ReentrantLock (virtual thread safety)
- Replace
ThreadLocal with ScopedValue where virtual threads are used
- Add defensive copies in record compact constructors for mutable components
- For full EJ item mapping: references/effective-java-modernization.md
When modernizing stream pipelines:
- Replace imperative accumulation loops with
Gatherers
- Use
windowFixed(n) for batch processing
- Use
windowSliding(n) for moving-window analysis
- Write custom
Gatherer for complex stateful transforms
- For examples: references/modern-api-patterns.md
Gotchas
- โ sealed class์ permits ์ ์ ๋ชจ๋ ์๋ธํ์
๋์ด ์คํจ โ exhaustive matching ๊นจ์ง
- โ record์ mutable ํ๋(List, Map) ์ง์ ์ ์ฅ โ ๋ฐฉ์ด์ ๋ณต์ฌ ํ์ (compact constructor)
- โ virtual thread์์
synchronized ์ฌ์ฉ โ pinning ๋ฐ์, ReentrantLock ์ฌ์ฉ
- โ pattern matching์์ guard ์กฐ๊ฑด ๋๋ฝ โ
case Foo f when f.bar() > 0 ํํ๋ก
Troubleshooting
| Symptom | Cause | Solution |
|---|
| Virtual thread hangs under load | Thread pinning from synchronized | Replace with ReentrantLock; detect with -Djdk.tracePinnedThreads=full |
NoSuchElementException from ScopedValue.get() | Accessed outside ScopedValue.where().run() | Ensure binding exists in the call chain; use isBound() to check |
| Compiler error "switch not exhaustive" | Missing case for sealed subtype | Add the missing case โ do NOT add default (loses exhaustiveness guarantee) |
IllegalArgumentException in record constructor | No validation in compact constructor | Add validation in compact constructor body |
| Virtual threads slower than platform threads | CPU-bound workload, not I/O-bound | Use platform threads / ForkJoinPool for CPU-bound work |
ClassCastException in pattern match | Stale pattern after sealed type change | Recompile all classes that switch on the sealed hierarchy |
| Stream Gatherer produces empty result | Gatherer.finisher() not invoked | Ensure terminal operation triggers the pipeline (e.g., .toList()) |
Spring @Async not using virtual threads | Custom executor overrides default | Configure AsyncConfigurer with Executors.newVirtualThreadPerTaskExecutor() |
| Gatherer state corrupted in parallel stream | Mutable shared state in Gatherer.ofSequential | Use Gatherer.of() with combiner, or ensure sequential stream |
Deep-Dive References
- Data-Oriented Programming โ Records, sealed types, ADTs, pattern matching, refactoring from OOP
- Virtual Threads & Concurrency โ Virtual threads, structured concurrency, scoped values, Spring Boot integration
- Modern API Patterns โ Stream Gatherers, Sequenced Collections, new JDK APIs
- Effective Java Modernization โ Effective Java items reinterpreted for Java 21+, migration guide from EJ-era patterns
Authoritative Sources
Patterns derived from:
- Brian Goetz โ "Data Oriented Programming in Java" (InfoQ, inside.java)
- Jose Paumard โ JEP Cafe series, "Clean Code with Records, Sealed Classes and Pattern Matching"
- Nicolai Parlog โ Inside Java Newscast, "All New Java Language Features Since Java 21"
- Viktor Klang โ Stream Gatherers design lead (inside.java)
- InfoQ Java Trends Report 2025 โ Industry adoption analysis
- OpenJDK JEPs โ 440 (Record Patterns), 441 (Pattern Matching for switch), 444 (Virtual Threads), 485 (Stream Gatherers), 505 (Structured Concurrency), 506 (Scoped Values)
- JDK 25 Performance Improvements โ inside.java (Claes Redestad, Per-Ake Minborg)