Java 21 is the most exciting Java release in years. Combined with Spring Boot 3.2+, it brings features that genuinely change how I write microservices โ not just syntax sugar, but fundamental performance and design improvements.
Here are the features I actually use every day and why they matter.
This is the biggest one. Before virtual threads, a Spring Boot app under high load would exhaust its thread pool waiting on I/O (DB calls, HTTP requests). Virtual threads are lightweight โ you can create millions of them without the overhead of platform threads.
Enabling them in Spring Boot 3.2 is just one config line:
# application.properties
spring.threads.virtual.enabled=true
The result in our loan eligibility API at Coforge: the same service handling 800 req/s scaled to 4,200 req/s without any code changes. Just virtual threads.
Gone are the days of verbose instanceof chains. Pattern matching makes domain logic dramatically cleaner:
// Old way
String describe(Object obj) {
if (obj instanceof String s) return "String: " + s.length();
else if (obj instanceof Integer i) return "Int: " + i;
else return "Unknown";
}
// Java 21 โ pattern matching switch
String describe(Object obj) {
return switch (obj) {
case String s -> "String of length " + s.length();
case Integer i -> "Integer: " + i;
case null -> "null value";
default -> "Unknown: " + obj.getClass().getSimpleName();
};
}
I use this constantly when handling different LLM response types, different event types in our event bus, and different error shapes from external APIs.
Records eliminate the boilerplate of DTO classes. Instead of a 40-line class with getters, setters, equals, hashCode, and toString, you get:
// A complete, immutable DTO in one line
public record TaxFilingRequest(
String userId,
int taxYear,
List<String> documentIds,
FilingStrategy strategy
) {}
// Works seamlessly with Jackson (Spring Boot)
// Works with @Valid Bean Validation
// Works with Spring Data projections
Sealed classes let you define a closed hierarchy of types โ perfect for modelling API results and errors:
public sealed interface FilingResult
permits FilingResult.Success, FilingResult.Failure, FilingResult.PendingReview {}
public record Success(String confirmationId, LocalDate filedDate)
implements FilingResult {}
public record Failure(String errorCode, String message)
implements FilingResult {}
public record PendingReview(String reviewId, String reason)
implements FilingResult {}
// At the call site โ exhaustive switch, no default needed:
String message = switch (result) {
case Success s -> "Filed! Confirmation: " + s.confirmationId();
case Failure f -> "Error: " + f.message();
case PendingReview p -> "Under review: " + p.reason();
};
When building the AI pipeline at Intuit, we needed to call multiple services in parallel and combine results. Structured concurrency (preview in Java 21) makes this clean and safe:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<UserDocuments> docs = scope.fork(() -> documentService.fetch(userId));
Future<TaxHistory> history = scope.fork(() -> taxService.getHistory(userId));
Future<RiskProfile> risk = scope.fork(() -> riskEngine.evaluate(userId));
scope.join().throwIfFailed(); // waits for all; cancels all if one fails
return new TaxContext(docs.get(), history.get(), risk.get());
}
Compare this to manually managing CompletableFuture chains โ structured concurrency is both safer (automatic cancellation on failure) and more readable.
| Feature | Impact | Use Case |
|---|---|---|
| Virtual Threads | 5ร throughput gain | I/O-bound microservices |
| Pattern Matching | ~40% less boilerplate | Domain logic, event handling |
| Records | ~80% less DTO code | API request/response DTOs |
| Sealed Classes | Compile-time exhaustiveness | Error modelling, result types |
| Structured Concurrency | Simpler parallel code | Multi-service aggregation |
eclipse-temurin:21-jre-alpine is a good starting point.