Core Java

Java 25 LTS: Complete Guide to Value Types, String Templates, Structured Concurrency & Everything New for Backend Engineers

Java 25 is the most transformative LTS release since Java 8. Project Valhalla ships. String Templates are finalized. Structured Concurrency graduates from preview. This guide covers every production-relevant feature with real code examples, migration steps, and a complete upgrade checklist for backend teams running Java 21.

Md Sanwar Hossain April 9, 2026 24 min read Core Java
Java 25 LTS new features guide for backend engineers — value types, string templates, structured concurrency

TL;DR — Java 25 in One Paragraph

"Java 25 LTS finalizes the biggest language and runtime changes in a decade: Value Types (Project Valhalla) eliminate object overhead for small data types, String Templates replace brittle string concatenation with type-safe interpolation, and Structured Concurrency with Scoped Values makes virtual-thread programming dramatically safer. If you're on Java 21, upgrading to Java 25 is the highest-ROI technical investment your team can make in 2026."

Table of Contents

  1. The Java LTS Evolution: 8 → 11 → 17 → 21 → 25
  2. Value Types: Project Valhalla Finally Ships
  3. String Templates Finalized (JEP 430)
  4. Structured Concurrency API Finalized (JEP 453)
  5. Scoped Values: Replacing ThreadLocal (JEP 446)
  6. Enhanced Pattern Matching
  7. Unnamed Classes & Instance Main Methods
  8. Performance Improvements
  9. Java 21 → 25 Migration Guide
  10. Spring Boot & Framework Compatibility
  11. Conclusion & Migration Checklist

1. The Java LTS Evolution: 8 → 11 → 17 → 21 → 25

Java's cadence shifted to a six-month release cycle in 2018, with Long-Term Support releases arriving every two years. Understanding what each LTS delivered helps you appreciate the compounding improvement that lands in Java 25.

The LTS Timeline at a Glance

LTS Release Year Headline Features Industry Impact
Java 8 2014 Lambdas, Streams, Optional, Date/Time API Functional Java; still used by ~35% of production as of 2025
Java 11 2018 var, HTTP Client, String methods, module system First post-8 mass migration target; killed Java 9/10
Java 17 2021 Sealed classes, Records, Pattern Matching instanceof, Text Blocks Modern Java baseline; Spring Boot 3 minimum requirement
Java 21 2023 Virtual Threads, Record Patterns, Switch Patterns, Sequenced Collections Concurrency revolution; enables 1M+ threads on commodity hardware
Java 25 2025 Value Types, String Templates, Structured Concurrency, Scoped Values Memory model revolution; decades of Valhalla research ships

Why Java 25 Is Different From Any Previous LTS

Every previous LTS added language sugar or new library APIs. Java 25 changes the runtime memory model itself. Project Valhalla's Value Types allow the JVM to store objects inline in arrays and on the stack — eliminating the pointer-chasing overhead that has burdened Java performance for 30 years. For backend engineers running high-throughput microservices, this means genuine performance improvements without changing application logic.

Additionally, three APIs that have been in preview since Java 21 (String Templates, Structured Concurrency, Scoped Values) all graduate to stable in Java 25. Teams that avoided preview features now have a clean, stable foundation to build on — without the "it might change next release" anxiety.

Java 21 vs Java 25: Feature Status Comparison

Feature Java 21 Java 25
Virtual Threads ✅ Stable (JEP 444) ✅ Stable + enhanced pinning fixes
String Templates ⚠️ Preview (JEP 430) ✅ Finalized
Structured Concurrency ⚠️ Preview (JEP 453) ✅ Finalized
Scoped Values ⚠️ Preview (JEP 446) ✅ Finalized
Value Types (Valhalla) ❌ Not available ✅ Stable (JEP 401)
Enhanced Pattern Matching ⚠️ Partial (records in switch) ✅ Full deconstruction + guard clauses
Unnamed Classes ⚠️ Preview (JEP 445) ✅ Finalized
Foreign Function & Memory API ✅ Stable (JEP 454) ✅ Stable + value type integration
Java 25 LTS new features overview: Value Types, String Templates, Structured Concurrency, Scoped Values, Pattern Matching
Java 25 LTS Feature Overview — from Project Valhalla value types to finalized Structured Concurrency and String Templates. Source: mdsanwarhossain.me

2. Value Types: Project Valhalla Finally Ships

Project Valhalla began in 2014 with a deceptively simple goal: make Java's type system work like primitive types for user-defined classes. After a decade of design iteration, JEP 401 delivers Value Classes — objects without identity that the JVM can store inline on the stack and in arrays, eliminating the pointer indirection and garbage collection pressure that accompanies every normal Java object.

What Is Object Identity and Why Does It Matter?

Every regular Java object has identity — a unique location in the heap that makes it distinguishable from all other objects with the same field values. This identity enables features like synchronized(obj), System.identityHashCode(), and == reference equality. But identity also forces every object to live on the heap, to have an object header (12–16 bytes), and to be accessed via a pointer — adding latency from cache misses and pressure on the garbage collector.

Value types deliberately sacrifice identity in exchange for these performance properties: stack allocation, inline storage in arrays, and no GC overhead. Think of them as user-defined primitives — as fast as int or double, but with the expressiveness of a class.

Declaring a Value Class: Before and After

// Java 21 — traditional reference class (heap-allocated, pointer-chased)
public final class Point {
    private final double x;
    private final double y;

    public Point(double x, double y) { this.x = x; this.y = y; }
    public double x() { return x; }
    public double y() { return y; }
    public double distanceTo(Point other) {
        double dx = this.x - other.x;
        double dy = this.y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
}
// Point[] points = new Point[1_000_000]; allocates 1M heap objects + 1M pointers

// Java 25 — value class (inline-allocated, zero GC pressure)
public value class Point {
    private final double x;
    private final double y;

    public Point(double x, double y) { this.x = x; this.y = y; }
    public double x() { return x; }
    public double y() { return y; }
    public double distanceTo(Point other) {
        double dx = this.x - other.x;
        double dy = this.y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
}
// Point[] points = new Point[1_000_000]; stores 16 bytes × 1M = 16MB contiguous!
// No object headers. No pointers. Cache-friendly. Zero GC pressure.

Value Class Rules and Restrictions

Where Value Types Shine in Backend Systems

The canonical use cases for value classes in backend Java code:

3. String Templates Finalized (JEP 430)

String Templates graduate from two preview rounds to a stable, production-ready feature in Java 25. They are not merely string interpolation — they are a type-safe, processor-extensible template mechanism designed to eliminate injection vulnerabilities while making multi-line string construction dramatically more readable.

The Three Built-In Template Processors

// STR — simple string interpolation (replaces StringBuilder / String.format)
String name = "Alice";
int age = 30;
String greeting = STR."Hello, \{name}! You are \{age} years old.";
// Result: "Hello, Alice! You are 30 years old."

// FMT — formatted interpolation with printf-style format specifiers
double price = 1234.5678;
String receipt = FMT."Total: $%,.2f\{price}";
// Result: "Total: $1,234.57"

// RAW — returns a StringTemplate object for custom processing
StringTemplate template = RAW."SELECT * FROM users WHERE id = \{userId}";
// Returns: StringTemplate with fragments ["SELECT * FROM users WHERE id = ", ""]
//          and values [userId]
// Use this as input to a SQL-safe template processor (see below)

Custom Template Processor: SQL Injection Prevention

The real power of String Templates is the ability to write domain-specific processors that validate, escape, or transform template values before assembly. This eliminates SQL injection at the language level:

// Safe SQL template processor — binds values as PreparedStatement parameters
public class SafeSQL implements StringTemplate.Processor<PreparedStatement, SQLException> {
    private final Connection connection;

    public SafeSQL(Connection connection) { this.connection = connection; }

    @Override
    public PreparedStatement process(StringTemplate st) throws SQLException {
        // Build the SQL with ? placeholders for each embedded value
        String sql = String.join("?", st.fragments());
        PreparedStatement ps = connection.prepareStatement(sql);
        List<Object> values = st.values();
        for (int i = 0; i < values.size(); i++) {
            ps.setObject(i + 1, values.get(i));  // Safe parameterized binding
        }
        return ps;
    }
}

// Usage — values are NEVER concatenated into SQL string directly
SafeSQL SQL = new SafeSQL(connection);
String userInput = "'; DROP TABLE users; --";  // Classic injection attempt
int userId = Integer.parseInt(userInput);       // Would throw, but even if not...
PreparedStatement ps = SQL."SELECT * FROM orders WHERE user_id = \{userId}";
// The processor creates: "SELECT * FROM orders WHERE user_id = ?"
// and binds userId as a parameter — injection is structurally impossible

JSON Template Processor Example

A JSON processor can auto-escape special characters in embedded values, preventing JSON injection attacks in API responses:

// JSON template processor with automatic escaping
StringTemplate.Processor<String, RuntimeException> JSON = st -> {
    StringBuilder sb = new StringBuilder();
    Iterator<String> fragments = st.fragments().iterator();
    for (Object value : st.values()) {
        sb.append(fragments.next());
        sb.append(jsonEscape(value));  // escape ", \, and control chars
    }
    sb.append(fragments.next());
    return sb.toString();
};

String userName = "John \"Johnny\" Doe";
String email = "john@example.com";
String json = JSON."""
    {
        "name": "\{userName}",
        "email": "\{email}",
        "active": \{true}
    }
    """;
// Properly escaped: {"name": "John \"Johnny\" Doe", "email": "john@example.com", "active": true}

Migration from String.format and StringBuilder

4. Structured Concurrency API Finalized (JEP 453)

Structured Concurrency is the companion feature to Virtual Threads (Java 21). Where virtual threads give you millions of cheap threads, Structured Concurrency gives you the discipline to manage them safely. It treats a group of concurrent tasks as a single unit of work — if any task fails, all others are automatically cancelled. No more thread leaks from forgotten executor cleanup.

The Problem Structured Concurrency Solves

Classic CompletableFuture fan-out with ExecutorService has a dangerous failure mode: if one task fails, other in-flight tasks keep running and consuming resources until they naturally finish — potentially minutes later. Consider this microservice aggregation pattern:

// Java 21 — dangerous: failing task doesn't cancel siblings
public OrderSummary getOrderSummary(long orderId) throws Exception {
    ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
    Future<Order> orderFuture     = executor.submit(() -> orderService.get(orderId));
    Future<Customer> custFuture   = executor.submit(() -> customerService.get(orderId));
    Future<Inventory> invFuture   = executor.submit(() -> inventoryService.check(orderId));

    // If orderService throws, custFuture and invFuture STILL RUN until done!
    // Thread leak: could exhaust virtual thread scheduler for 30+ seconds
    Order order     = orderFuture.get();    // may throw after 2s
    Customer cust   = custFuture.get();     // wasteful — already ran
    Inventory inv   = invFuture.get();
    return new OrderSummary(order, cust, inv);
}

// Java 25 — structured: failure cancels all siblings immediately
public OrderSummary getOrderSummary(long orderId) throws Exception {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Subtask<Order>     orderTask = scope.fork(() -> orderService.get(orderId));
        Subtask<Customer>  custTask  = scope.fork(() -> customerService.get(orderId));
        Subtask<Inventory> invTask   = scope.fork(() -> inventoryService.check(orderId));

        scope.join()           // wait for all tasks
             .throwIfFailed(); // propagate first exception, cancel siblings

        // All three succeeded — safe to unwrap
        return new OrderSummary(orderTask.get(), custTask.get(), invTask.get());
        // scope.close() called by try-with-resources: cancels any stragglers
    }
}

ShutdownOnSuccess: First-Wins Fan-Out

ShutdownOnSuccess cancels all remaining tasks the moment the first one succeeds — perfect for hedged requests or leader election patterns:

// Send the same request to two replica regions; use the fastest response
public UserProfile fetchFastest(long userId) throws Exception {
    try (var scope = new StructuredTaskScope.ShutdownOnSuccess<UserProfile>()) {
        scope.fork(() -> usEastService.getUser(userId));
        scope.fork(() -> euWestService.getUser(userId));

        scope.join();  // blocks until first success; cancels the slower task
        return scope.result();  // returns the winning result
    }
}

Spring Boot Microservices Integration Pattern

Structured concurrency works naturally in Spring Boot 4.x with virtual threads enabled. Annotate the Spring configuration with @EnableVirtualThreads and use StructuredTaskScope directly in @Service methods — Spring's transaction management and MDC logging context propagate automatically through the scope boundary thanks to Scoped Values integration.

5. Scoped Values: Replacing ThreadLocal (JEP 446)

ThreadLocal was designed for platform threads in 1998. It stores a mutable, per-thread value that persists for the entire thread lifetime. In the virtual threads world — where a single HTTP request may be handled by thousands of short-lived virtual threads — ThreadLocal has two critical problems: inherited values aren't cleaned up automatically (memory leaks), and child threads inherit a mutable copy that can diverge unexpectedly.

Scoped Values vs ThreadLocal

// ThreadLocal — problematic with virtual threads
private static final ThreadLocal<User> CURRENT_USER = new ThreadLocal<>();

public void handleRequest(User user) {
    CURRENT_USER.set(user);          // must remember to call remove()!
    try {
        processOrder();              // reads CURRENT_USER.get()
    } finally {
        CURRENT_USER.remove();       // easy to forget → memory leak in thread pools
    }
}

// ScopedValue — immutable, auto-cleaned, virtual-thread-safe
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();

public void handleRequest(User user) {
    ScopedValue.where(CURRENT_USER, user)
               .run(() -> processOrder());
    // CURRENT_USER is automatically unavailable after run() returns
    // No finally block, no memory leak, no mutable state
}

// Nested scopes: child scope shadows parent — parent value is restored on exit
ScopedValue.where(CURRENT_USER, adminUser)
    .run(() -> {
        // CURRENT_USER.get() == adminUser here
        ScopedValue.where(CURRENT_USER, guestUser)
            .run(() -> {
                // CURRENT_USER.get() == guestUser here (shadows admin)
            });
        // CURRENT_USER.get() == adminUser again — automatically restored
    });

ThreadLocal to ScopedValue Migration Guide

6. Enhanced Pattern Matching

Java 21 introduced record patterns and switch expressions with pattern matching. Java 25 completes the picture with full deconstruction patterns, guard clauses using when, and deeply nested patterns. The result is a switch expression as expressive as Kotlin's when or Scala's match.

Deconstruction Patterns with Guard Clauses

sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}

// Java 25 — full deconstruction + guard clauses + nested patterns
public String describe(Shape shape) {
    return switch (shape) {
        case Circle(var r) when r == 0       -> "degenerate zero-radius circle";
        case Circle(var r) when r < 0        -> throw new IllegalStateException("negative radius");
        case Circle(var r)                   -> STR."circle with area \{Math.PI * r * r:.2f}";
        case Rectangle(var w, var h) when w == h -> STR."square with side \{w}";
        case Rectangle(var w, var h)         -> STR."rectangle \{w}×\{h}";
        case Triangle(var b, var h)          -> STR."triangle area \{0.5 * b * h}";
    };  // compiler verifies exhaustiveness — no default needed for sealed types
}

// Nested record deconstruction
record Order(Customer customer, Address shippingAddress) {}
record Customer(String name, String tier) {}
record Address(String city, String country) {}

String routeOrder(Order order) {
    return switch (order) {
        case Order(Customer(_, "PREMIUM"), Address(_, "US"))
            -> "expedited-us";
        case Order(Customer(var name, "PREMIUM"), Address(var city, var country))
            -> STR."expedited-international:\{country}";
        case Order(Customer(var name, _), Address(_, var country))
            -> STR."standard:\{country}";
    };
}

Pattern Matching in instanceof (Stable Since Java 16, Enhanced in 25)

Java 25 extends instanceof pattern binding to support deconstruction: if (shape instanceof Circle(var r) && r > 10) binds r in the if-body without an explicit cast. Combined with the when guard in switch, this eliminates virtually all manual type checks and casts from idiomatic Java code.

7. Unnamed Classes & Instance Main Methods (JEP 445)

Java 25 finalizes Unnamed Classes and Instance Main Methods — a feature explicitly targeted at beginners and scripting use cases, not production services. Understanding its scope prevents misapplication.

What's New: Simplified Program Entry Points

// Java 21 — minimum viable "Hello World" requires class boilerplate
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

// Java 25 — unnamed class: no class declaration needed for a single-file program
void main() {
    System.out.println("Hello, World!");
}
// Run directly: java Hello.java

// Instance main method — main() can be an instance method (no static required)
// The JVM creates an instance of the enclosing class to call it
class Greeter {
    String message = "Hello from an instance!";
    void main() {
        System.out.println(message);  // accesses instance field — no static context
    }
}

When to Use (and Not Use) Unnamed Classes

8. Performance Improvements

Java 25 delivers measurable performance improvements across several dimensions beyond Value Types. These gains compound with the virtual thread improvements from Java 21 and the GC improvements shipped across Java 22–24.

Value Types: Memory and GC Impact

JVM Startup Time Improvements

Java 25 ships with CRaC (Coordinated Restore at Checkpoint) improvements and enhanced AOT (Ahead-of-Time) metadata that reduce cold-start latency for serverless and container workloads. A typical Spring Boot 4.x application that starts in 2.1s on Java 21 starts in approximately 1.4s on Java 25 with these enhancements — a 33% improvement without GraalVM native compilation.

Virtual Thread Pinning Fixes

Java 21 shipped virtual threads with a known limitation: virtual threads could become "pinned" to their carrier platform thread when inside a synchronized block, defeating the scalability benefits. Java 25 resolves virtual thread pinning for synchronized blocks — the last major blocker preventing drop-in adoption of virtual threads in legacy codebases that use synchronized extensively. This alone unblocks virtual thread adoption for thousands of teams.

String Templates Performance

STR. template processing is JIT-compiled to be as fast as or faster than equivalent StringBuilder chains, and approximately 15% faster than String.format() in hot loops, because the JVM intrinsifies template processing when the processor is one of the built-in types.

9. Java 21 → 25 Migration Guide

The Java 21 → 25 migration is significantly smoother than Java 11 → 17 or Java 8 → 11. All new features are additive and backward-compatible. The main migration work involves: updating build tooling, fixing deprecation warnings, and updating Docker base images.

Step 1 — Maven and Gradle Configuration

<!-- Maven pom.xml — update Java version -->
<properties>
    <java.version>25</java.version>
    <maven.compiler.source>25</maven.compiler.source>
    <maven.compiler.target>25</maven.compiler.target>
</properties>

<!-- maven-compiler-plugin — enable preview features if using value classes -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.13.0</version>
    <configuration>
        <release>25</release>
        <!-- Only needed if using preview features -->
        <!-- <compilerArgs><arg>--enable-preview</arg></compilerArgs> -->
    </configuration>
</plugin>

# Gradle build.gradle.kts
java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(25))
    }
}

tasks.withType<JavaCompile>().configureEach {
    options.release.set(25)
}

Step 2 — Docker Base Image

# Dockerfile — update from Java 21 to Java 25
# Before (Java 21):
FROM eclipse-temurin:21-jre-alpine AS runtime

# After (Java 25):
FROM eclipse-temurin:25-jre-alpine AS runtime

# Or use distroless for security-hardened production images:
FROM gcr.io/distroless/java25-debian12:nonroot AS runtime

# Multi-stage build pattern (unchanged between versions)
FROM eclipse-temurin:25-jdk-alpine AS builder
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests

FROM eclipse-temurin:25-jre-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

Breaking Changes and Removed APIs

Compiler Warning Cleanup

Run your build with -Xlint:all to surface all deprecation and compatibility warnings. Common issues in Java 21 codebases targeting Java 25:

10. Spring Boot & Framework Compatibility

The Java 25 LTS release is timed to align with Spring Boot 4.x — a major Spring release that fully embraces virtual threads, Structured Concurrency, and Value Types as first-class citizens. Here's what the framework ecosystem looks like at Java 25 launch.

Spring Boot 4.x + Java 25

Quarkus and Micronaut

JVM Args Changes for Java 25

JVM Flag Java 21 Status Java 25 Status
--enable-preview Required for preview features Only needed for new Java 25 previews
-Djdk.virtualThreadScheduler.parallelism Default = CPU count Default = CPU count (unchanged)
-XX:+UseZGC Opt-in generational ZGC Generational ZGC is now default GC
-Xss (thread stack size) 1MB default for platform threads Unchanged; virtual threads start at ~512 bytes
--add-opens (deep reflection) Required by some frameworks Fewer needed; Spring 4.x reduced dependency

GraalVM Native Image & Java 25

GraalVM 25.0 (aligned with JDK 25) adds native image support for Value Classes and Structured Concurrency. Value class arrays are flattened in native image memory layout, delivering the same cache-efficiency benefits as on the JVM. Startup time for GraalVM native Spring Boot applications drops to ~50ms with Java 25 — a significant improvement for serverless deployments where cold starts are billed.

11. Conclusion & Migration Checklist

Java 25 LTS is the most significant release in the platform's history since Java 8. The combination of Project Valhalla (Value Types), finalized String Templates, Structured Concurrency, and Scoped Values addresses four different layers of the Java stack simultaneously — the memory model, the string API, the concurrency model, and the context propagation model.

For backend engineers on Java 21, the upgrade investment is low (mostly tooling updates) and the returns are high: lower GC pressure, safer concurrent code, elimination of ThreadLocal footguns, and a cleaner string API that prevents injection vulnerabilities by construction. This is not a "nice-to-have" upgrade — it is the new foundation for production Java for the next four years.

Key Takeaways

Java 21 → 25 Upgrade Checklist (12 Items)

  1. Update pom.xml / build.gradle to java.version=25 and release=25
  2. Update Docker base images from eclipse-temurin:21 to eclipse-temurin:25
  3. Upgrade Spring Boot to 4.x (or Quarkus/Micronaut to their Java-25-compatible versions)
  4. Run mvn verify -Xlint:all — fix all deprecation warnings before proceeding
  5. Remove any Object.finalize() overrides — replace with java.lang.ref.Cleaner
  6. Replace SecurityManager references (already removed)
  7. Remove --enable-preview flags for APIs now stable in Java 25 (String Templates, Structured Concurrency, Scoped Values)
  8. Audit ThreadLocal usages — migrate request-scoped ones to ScopedValue
  9. Replace String.format() and string concatenation in hot paths with STR. templates
  10. Identify small, immutable value-like classes (Money, ID wrappers, coordinates) — convert to value class
  11. Replace multi-task CompletableFuture.allOf() patterns with StructuredTaskScope
  12. Verify synchronized-heavy code paths work correctly with virtual threads (pinning now fixed — test throughput)

Leave a Comment

Related Posts

Md Sanwar Hossain - Software Engineer
Md Sanwar Hossain

Software Engineer · Java · Spring Boot · Microservices · AI/LLM Systems

All Posts
Last updated: April 9, 2026