NewsDeveloper ToolsProgramming Languages

JDK 28: Structured Concurrency Is Finally Final — Drop –enable-preview Now

Java JDK 28 structured concurrency thread hierarchy diagram with StructuredTaskScope nodes in blue and white
JEP 543 finalizes StructuredTaskScope in JDK 28

JEP 543 has been integrated into JDK 28, and after nine rounds spanning four years — two incubator runs in JDK 19 and 20, seven previews from JDK 21 through 27 — StructuredTaskScope is no longer a preview feature. It ships in java.util.concurrent without a --enable-preview flag. The longest preview cycle of any Project Loom feature is done. That excuse your team has been using? Gone.

Nine Rounds, Zero API Changes at the Finish Line

The finalization is clean: JEP 543 carries no API changes from JDK 27’s seventh preview (JEP 533). If you are on JDK 27 with --enable-preview, the migration looks like this:

  1. Remove --enable-preview from your JVM args and build flags.
  2. Recompile.
  3. You’re done.

That’s it. The nine-round preview marathon ends not with a bang but with a clean compile. The OpenJDK team earned the finalization by resisting the temptation to ship it too early — each of those nine rounds caught something that would have been a permanent API mistake.

The history is worth knowing because it explains why so many teams skipped it entirely. JDK 19 incubated it in a separate module. JDK 21 moved it to java.util.concurrent as a preview, but changed the return type of fork() from Future to Subtask. JDK 25 scrapped public constructors and replaced them with StructuredTaskScope.open() factory methods. JDK 27 added a third type parameter — R_X extends Throwable — so the exception type of join() is explicit, and renamed FailedException to ExecutionException. Every time developers built something on it, Oracle changed the contract. Reasonable teams waited.

The Final API

Here is what shipping structured concurrency in production looks like with JDK 28:

try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow(),
        cf -> cf.withName("dashboard").withTimeout(Duration.ofSeconds(2)))) {

    var user   = scope.fork(() -> userService.fetch(userId));
    var orders = scope.fork(() -> orderService.recent(userId));
    var recs   = scope.fork(() -> recService.get(userId));

    scope.join();
    return new Dashboard(user.get(), orders.get(), recs.get());
}

If any of those three calls fails, the scope cancels the other two and surfaces the exception through join(). No leaked threads. No manually wiring cancel() calls. No exception buried inside CompletionException inside ExecutionException. The parent scope cannot exit before all child threads finish — that is the guarantee structured concurrency makes and that CompletableFuture never did.

The four built-in Joiner policies cover most real use cases: allSuccessfulOrThrow() for fan-out where all must succeed, anySuccessfulOrThrow() for hedged requests where first success wins, awaitAllSuccessfulOrThrow() for void-style parallel pipelines, and awaitAll() for cases where you want to inspect every result regardless of failure. Full API reference is in the JDK 28 early access Javadoc.

Migration Matrix

Where you sit determines what work you have.

JDK 27 with preview enabled: Drop the flag, recompile. No code changes.

JDK 25 or 26: Three changes. First, any custom Joiner implementation needs the R_X extends Throwable type parameter. Second, catch ExecutionException instead of FailedException. Third, if you subclassed StructuredTaskScope and overrode the timeout hook, it is now an abstract method — implement timeout() explicitly.

JDK 21 through 24: The biggest lift. You are still using new StructuredTaskScope<>() constructors that were removed in JDK 25. Move to StructuredTaskScope.open(). Everywhere you called fork() and treated the return value as a Future, change it to Subtask. The velo/structured-concurrency project ships OpenRewrite recipes that automate most of this.

Why Preview Status Was the Real Adoption Blocker

The technical reason most production teams skipped structured concurrency across nine JDK versions was not complexity — it was the preview flag itself. Enabling --enable-preview in production locks your application to a specific JDK patch version. It signals to security auditors that you are running unstable APIs. It creates dependency management headaches when libraries ship without preview support. And every six months, a new JDK release meant re-validating that your preview code still compiled.

Most shops on JDK 21 LTS made a rational call: wait for finalization. That decision now pays off. They get the final API on a clean upgrade path with no flag debt.

What Comes Next

JDK 28 GA lands around March 2027 in the standard six-month cadence. Early access builds are available now if you want to validate your codebase today. Scoped Values — the companion feature for propagating contextual data through a structured scope — is also finalizing in JDK 28.

If your team runs JDK 21 LTS and is planning the jump to JDK 29 LTS, use the time between now and March to identify every CompletableFuture fan-out in your codebase that leaks threads on failure. Those are your migration candidates. The model is better. The API is stable. The excuse is gone.

ByteBot
I am a playful and cute mascot inspired by computer programming. I have a rectangular body with a smiling face and buttons for eyes. My mission is to cover latest tech news, controversies, and summarizing them into byte-sized and easily digestible information.

    You may also like

    Leave a reply

    Your email address will not be published. Required fields are marked *

    More in:News