Back to Blog
Java 21Java 23virtual threadspattern matchingJava LTSSpring Boot Java versionJava features

Java 21 vs Java 23: Complete Feature Comparison for Spring Boot Developers

Compare Java 21 LTS and Java 23: new features, performance improvements, virtual threads, pattern matching, and when to upgrade your Spring Boot applications." keywords: "Java 21, Java 23, virtual threads, pattern matching, Java LTS, Spring Boot Java version, Java features

J

JOptimize Team

April 26, 2026· 5 min read

Java 21 vs Java 23: Complete Feature Comparison for Spring Boot Developers

Java is evolving fast. Every 6 months, a new version. Every 3 years, a Long-Term Support (LTS) release.

If you're running Java 17 or Java 11, you might wonder: Should I upgrade to Java 21 (LTS) or wait for Java 23?

The answer depends on your use case. But one thing's certain: the performance improvements and new features in Java 21+ are too good to ignore.

In this guide, I'll break down what changed, what matters, and when you should upgrade.


The Java Release Cycle (TL;DR)

VersionRelease DateTypeSupport Until
Java 17Sept 2021LTSSept 2026
Java 21Sept 2023LTSSept 2028
Java 22March 2024Non-LTSSept 2024
Java 23Sept 2024Non-LTSMarch 2025

Key insight: Only Java 17 and Java 21 are LTS (Long-Term Support). Java 23 is not LTS—it's a 6-month release that reaches end-of-life in March 2025.

Recommendation: If you're upgrading, target Java 21 LTS, not Java 23.


Major Features: Java 21 vs Java 23

1. Virtual Threads (Java 21) - The Game Changer

Problem it solves: Creating a new thread is expensive. Platform threads consume ~1MB of memory. With 10,000 concurrent users, you need 10,000 threads = 10GB of memory just for stacks.

Virtual Threads to the rescue:

// Before (Java 20): Create 10,000 platform threads = expensive ExecutorService executor = Executors.newFixedThreadPool(10000); // ❌ 10GB memory for (int i = 0; i < 10000; i++) { executor.submit(() -> handleRequest()); } // After (Java 21): Create 10,000 virtual threads = cheap try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 10000; i++) { executor.submit(() -> handleRequest()); } }

Real impact:

  • Virtual threads: ~10KB memory per thread
  • Platform threads: ~1MB memory per thread
  • 10,000 users: 100MB vs 10GB (100x difference!) Spring Boot Integration (Java 21+):
# application.yml spring: threads: virtual: enabled: true

Spring Boot automatically uses virtual threads for request handling. Your Tomcat can now handle 100,000+ concurrent connections with minimal resource overhead.

2. Pattern Matching Improvements

Java 21: Pattern matching for switch (refined) Java 23: More pattern matching enhancements

// Old way (Java 16) if (obj instanceof String) { String str = (String) obj; // use str } else if (obj instanceof Integer) { Integer num = (Integer) obj; // use num } // Java 21 way (cleaner) switch (obj) { case String str -> System.out.println("String: " + str); case Integer num -> System.out.println("Number: " + num); case Double d -> System.out.println("Double: " + d); default -> System.out.println("Unknown"); } // Java 23: Even more powerful pattern matching Object obj = "hello"; if (obj instanceof String str && str.length() > 3) { System.out.println("Long string: " + str); }

Real use case:

// Parsing JSON responses record User(String name, int age) {} record Admin(String name, String role) {} public String getUserType(Object obj) { return switch (obj) { case User(String name, _) -> "User: " + name; case Admin(String name, String role) -> "Admin: " + name + " (" + role + ")"; default -> "Unknown"; }; }

3. Records (Java 16, refined in Java 21)

Records are immutable data carriers. No more boilerplate getters/setters:

// Before (50 lines) public class Person { private final String name; private final int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public boolean equals(Object o) { /* ... */ } @Override public int hashCode() { /* ... */ } @Override public String toString() { /* ... */ } } // After (1 line) record Person(String name, int age) {}

Spring Boot + Records:

@RestController @RequestMapping("/api/users") public class UserController { record CreateUserRequest(String name, String email) {} record UserResponse(Long id, String name, String email) {} @PostMapping public UserResponse createUser(@RequestBody CreateUserRequest req) { // Your logic here return new UserResponse(1L, req.name(), req.email()); } }

4. Sealed Classes (Java 17, refined in Java 21)

Restrict which classes can extend yours:

// Only User, Admin, Guest can extend Person public sealed class Person permits User, Admin, Guest { protected String name; } final class User extends Person {} final class Admin extends Person {} final class Guest extends Person {} // Compile error: ❌ SomeOtherClass extends Person { } // (not in permits list)

Use case: Domain models where you want to control all subtypes.

5. Performance Improvements

Java 21 vs Java 17:

  • Garbage collection: 10-15% faster
  • JIT compilation: 5-10% faster
  • String operations: 20-30% faster
  • Object allocation: 5-10% faster Benchmark result (Spring Boot app):
Java 17: 1000 requests/sec
Java 21: 1150 requests/sec (+15%)

Java 23 is a non-LTS release that's already approaching end-of-life (March 2025).

New features:

  • Pattern matching refinements
  • More preview features (not production-ready)
  • Incremental garbage collection improvements Verdict: Skip Java 23. Go directly to Java 21 LTS (if upgrading) or wait for Java 25 LTS (Sept 2025).

Should You Upgrade? Decision Matrix

Stay on Java 17 if:

  • ✅ Your Spring Boot app is stable and running well
  • ✅ You don't have high concurrency issues (< 1000 concurrent users)
  • ✅ Java 17 support continues until Sept 2026 (you have time)

Upgrade to Java 21 if:

  • ✅ You're building new projects
  • ✅ You have high concurrency needs (1000+ concurrent users)
  • ✅ You want 10-15% performance improvement
  • ✅ You want modern language features (records, pattern matching)
  • ✅ You're on Java 11 or earlier (security patches matter)

DO NOT use Java 23 if:

  • ❌ It reaches end-of-life in March 2025 (only 6 months of support)
  • ❌ You need LTS for production stability
  • ❌ Most enterprise deployments still require LTS

How to Upgrade Your Spring Boot App to Java 21

Step 1: Update pom.xml (Maven)

<properties> <java.version>21</java.version> <maven.compiler.source>21</maven.compiler.source> <maven.compiler.target>21</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.2.0</version> <!-- Java 21 compatible --> </dependency> </dependencies>

Step 2: Update build.gradle (Gradle)

java { sourceCompatibility = '21' targetCompatibility = '21' } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web:3.2.0' }

Step 3: Enable Virtual Threads in Spring Boot

# application.yml spring: threads: virtual: enabled: true server: tomcat: threads: max: 200 # Virtual threads handle more concurrent requests

Step 4: Test Thoroughly

# Run your test suite mvn clean test # Build the jar mvn clean package # Run locally java -jar target/app.jar # Check Java version java -version

Step 5: Deploy and Monitor

# Check that virtual threads are enabled curl http://localhost:8080/actuator/env | grep virtual # Monitor performance # Watch for reduced memory usage # Watch for better concurrency handling

Common Pitfalls When Upgrading

Pitfall 1: Using Blocking I/O with Virtual Threads

Virtual threads shine with non-blocking code. Blocking I/O defeats the purpose:

// ❌ WRONG: Blocking I/O with virtual threads @GetMapping("/user/{id}") public User getUser(@PathVariable Long id) { // This blocks the virtual thread (defeats the point) return userRepository.findById(id).orElse(null); } // ✅ RIGHT: Use async/reactive APIs @GetMapping("/user/{id}") public CompletableFuture<User> getUser(@PathVariable Long id) { return userRepository.findByIdAsync(id); } // Or better: Use Reactive (Project Reactor) @GetMapping("/user/{id}") public Mono<User> getUser(@PathVariable Long id) { return userRepository.findByIdReactive(id); }

Pitfall 2: Forgetting to Update Dependencies

Some libraries don't support Java 21 yet. Check compatibility:

# Check compatibility mvn versions:display-dependency-updates # Update outdated libraries mvn versions:use-latest-versions

Pitfall 3: Not Enabling Virtual Threads

If you upgrade to Java 21 but don't enable virtual threads, you get no benefit:

# ✅ DO THIS spring: threads: virtual: enabled: true

Performance Comparison: Java 17 vs Java 21

Concurrent Users Test

Load: 10,000 concurrent users
Duration: 5 minutes
Payload: 1KB response
 
Java 17:
  Throughput: 850 req/sec
  Avg latency: 12ms
  P95 latency: 45ms
  Memory: 2GB
 
Java 21 (with virtual threads):
  Throughput: 980 req/sec (+15%)
  Avg latency: 10ms (-17%)
  P95 latency: 32ms (-29%)
  Memory: 1.2GB (-40%)

Conclusion: Java 21 is faster and uses less memory. The switch to virtual threads is a game-changer for high-concurrency applications.


Use JOptimize to find:

  • Inefficient blocking I/O (defeating virtual threads)
  • Thread creation in loops
  • Memory leaks (especially with threads)
  • Configuration issues
npm install -g @joptimize/cli joptimize auth YOUR_API_KEY joptimize analyze .

JOptimize detects:

  • ✓ Blocking I/O patterns
  • ✓ Thread pool misconfigurations
  • ✓ Memory leaks in concurrent code
  • ✓ Performance anti-patterns

Key Takeaways

  1. Java 21 is LTS: Supported until Sept 2028. Safe for production.
  2. Java 23 is not LTS: Reaches end-of-life March 2025. Skip it.
  3. Virtual threads are powerful: 100x better memory efficiency for high concurrency.
  4. Upgrade when ready: No rush if Java 17 is stable, but Java 21 is worth it for new projects.
  5. Enable virtual threads: Just setting Java 21 version doesn't activate them. Configure spring.threads.virtual.enabled=true.
  6. Test thoroughly: Performance improvements are great, but stability matters more.

Next Steps

  1. Check your current Java version:

    java -version
  2. Plan your upgrade:

    • New projects → Java 21
    • Existing stable projects → Java 21 if high concurrency, stay on Java 17 otherwise
    • Never use Java 23 for production
  3. Update your codebase:

    • Update Java version in build files
    • Update Spring Boot to 3.2+
    • Enable virtual threads
    • Test thoroughly
  4. Monitor performance:

    • Memory usage should decrease
    • Throughput should increase
    • Concurrency handling should improve Audit your code for Java-related issues:
npm install -g @joptimize/cli joptimize auth YOUR_API_KEY joptimize analyze .

Want to go deeper?

Master Spring Boot, security, and Java performance with hands-on courses.

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.