☕ Java

Scoped Values

Scoped Values, finalized in Java 25 (JEP 506, after several preview iterations starting with JEP 429 in Java 20), provide ScopedValue<T> as a mechanism for safely and efficiently sharing immutable, per-operation data with called methods and, critically, with virtual-thread subtasks forked via structured concurrency — addressing both the mutability hazards and the high per-virtual-thread memory cost that made the older ThreadLocal mechanism a poor fit once an application might be running with enormous numbers of concurrent virtual threads. This entry covers exactly what was wrong with ThreadLocal for this purpose, ScopedValue's bound-for-the-duration-of-a-call model via where()/run()/call(), how scoped value bindings are automatically inherited by subtasks forked in a StructuredTaskScope, and the deliberate restrictions (no general mutation, no unbind-and-rebind in arbitrary code) that give Scoped Values their safety and performance advantages over ThreadLocal.

Motivation — Where ThreadLocal Falls Short for This Purpose

ThreadLocal<T>, available since Java 1.2, has long served as the standard way to associate a per-thread value with code that runs on that thread without needing to pass that value as an explicit parameter through every intervening method call — commonly used for things like a request ID, a security context, or a transaction handle that many layers of unrelated code might need to consult, where threading it through every method signature along the call chain would be invasive and impractical. But ThreadLocal has two characteristics that became considerably more problematic once virtual threads made it practical to run enormous numbers of concurrent threads. First, ThreadLocal values are mutable by design — any code with a reference to the ThreadLocal can call set(...) at any point, from anywhere, potentially long after the value was first established, with no way for a reader of the value to be confident it hasn't been changed by some unrelated code earlier in the same thread's execution; this openness to arbitrary mutation, from anywhere, at any time, is a real source of bugs in large codebases, where a ThreadLocal's value at the point it's read can depend on an easily-overlooked chain of earlier set() calls scattered across the code. Second, and more specifically relevant to virtual threads, every ThreadLocal a thread has ever used effectively requires that thread to carry its own per-thread storage for it for as long as the thread exists, and this overhead, while a relatively minor and rarely-noticed cost for the comparatively small number of platform threads an application traditionally ran, becomes meaningfully more significant when multiplied across potentially millions of concurrent virtual threads — a cost the virtual-thread design specifically wanted to avoid reintroducing, since virtual threads were built around the premise of being cheap enough to create in huge numbers. Scoped Values, introduced via JEP 429 and finalized as JEP 506 in Java 25, address both issues directly: a ScopedValue<T>'s value is bound only for the dynamic extent of a specific call — established via where(...).run(...) or where(...).call(...), readable by the bound code and anything it calls (including, critically, subtasks forked from within a structured concurrency scope), and automatically and unconditionally unbound the instant that call returns — with no general set() method available at all, removing the "changed unpredictably from somewhere else" hazard ThreadLocal carries, and with an implementation specifically designed to be lightweight enough to bind cheaply on a per-virtual-thread, per-operation basis at the scale virtual threads are meant to support.
Java
// ── ThreadLocal — mutable, settable from anywhere, at any time ─────────
static final ThreadLocal<String> requestId = new ThreadLocal<>();

void handleRequest() {
    requestId.set("req-123");          // set once here...
    doSomeWork();
    // ... but ANY code, anywhere, with a reference to "requestId" could call
    // requestId.set("something-else") at any later point in this same thread's
    // execution — no guarantee the value read later is still what was set here
}

void doSomeWork() {
    System.out.println(requestId.get());   // relies on trusting nobody changed it in between
}

// ── Scoped Values — bound only for a specific call's dynamic extent ────
static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

void handleRequest() {
    ScopedValue.where(REQUEST_ID, "req-123")
        .run(() -> doSomeWork());   // REQUEST_ID is bound to "req-123" ONLY for this call
    // Once run() returns, REQUEST_ID is automatically, unconditionally unbound here —
    // no lingering value, no way it could have been silently changed mid-call from elsewhere
}

void doSomeWork() {
    System.out.println(REQUEST_ID.get());   // "req-123" — guaranteed stable for this entire call
}
// REQUEST_ID.get();   // called OUTSIDE any where(...).run(...) binding — throws
// NoSuchElementException, since there is no general set()/unbind() to leave a stale value around

Binding and Reading — where(), run(), call(), and orElse()

A ScopedValue<T> instance is created via the static factory ScopedValue.newInstance(), typically held as a static final field much like a ThreadLocal traditionally was. Binding a value to it is done via the static ScopedValue.where(scopedValue, value) method, which returns a Carrier that can chain additional .where(...) calls to bind several scoped values at once, and which is then executed via .run(runnable) (for code with no return value) or .call(callable) (for code that produces a result) — the bound value (or values) is in effect only for the dynamic extent of that run()/call() invocation: readable by the lambda passed to run()/call() itself, and by anything that lambda calls, transitively, through any depth of method calls, but the binding is fully and automatically removed the instant run()/call() returns, with no separate cleanup step required and no possibility of the binding being left dangling by a forgotten unbind call, since there is no separate unbind operation to forget in the first place. Reading a scoped value's current binding is done via get(), which throws NoSuchElementException if called from code that isn't currently within any where(...).run()/call() binding for that particular ScopedValue — analogous to calling get() on a ThreadLocal that was never set(), except that a ScopedValue's "unbound" state is the unavoidable default rather than something that depends on whether some earlier code happened to call set() first. isBound() can check whether a binding is currently active without throwing, and orElse(defaultValue) (alongside orElseThrow(exceptionSupplier) for a custom exception) provides a convenient way to read a scoped value with a fallback when it might not be bound, reading cleanly as a single expression rather than requiring a separate isBound() check followed by a conditional get(). Rebinding a ScopedValue to a different value for a nested portion of code is fully supported and is the only way a scoped value's effective binding ever changes within a single call chain — a nested where(...).run(...)/call() with the same ScopedValue establishes a new binding for the duration of that nested call, and once that nested call returns, the previous (outer) binding is restored automatically, exactly mirroring how a method's local variable shadowing inside a nested block reverts once that block exits.
Java
static final ScopedValue<String> USER_CONTEXT = ScopedValue.newInstance();
static final ScopedValue<String> TRACE_ID = ScopedValue.newInstance();

// ── Binding via where().run() / where().call() ──────────────────────────
void processRequest() {
    ScopedValue.where(USER_CONTEXT, "alice")
        .where(TRACE_ID, "trace-789")     // chaining multiple bindings in one Carrier
        .run(() -> handleBusinessLogic());
}

String computeSomething() {
    return ScopedValue.where(USER_CONTEXT, "bob")
        .call(() -> "Processed for " + USER_CONTEXT.get());   // .call() returns a value
}

// ── Reading — get(), isBound(), orElse() ────────────────────────────────
void handleBusinessLogic() {
    System.out.println(USER_CONTEXT.get());   // "alice" — bound by the enclosing where().run()

    if (TRACE_ID.isBound()) {
        System.out.println("Trace: " + TRACE_ID.get());
    }
}

void mightNotBeBound() {
    // USER_CONTEXT.get();   // NoSuchElementException if called with NO active binding at all
    String user = USER_CONTEXT.orElse("anonymous");   // safe fallback, no exception
}

// ── Nested rebinding — restored automatically once the nested call returns
void outer() {
    ScopedValue.where(USER_CONTEXT, "alice").run(() -> {
        System.out.println(USER_CONTEXT.get());   // "alice"

        ScopedValue.where(USER_CONTEXT, "bob").run(() -> {
            System.out.println(USER_CONTEXT.get());   // "bob" — nested rebinding in effect
        });

        System.out.println(USER_CONTEXT.get());   // back to "alice" — outer binding restored
        // automatically the instant the nested run() call above returned
    });
}

Automatic Inheritance by Structured Concurrency Subtasks

The specific design point that most directly connects Scoped Values to structured concurrency is automatic inheritance: subtasks forked within a StructuredTaskScope automatically inherit whatever ScopedValue bindings were active for the scope's owner thread at the moment the scope was opened — if the scope's owner has a particular ScopedValue bound to a given value, then every subtask forked into that scope sees that exact same value when it reads the same ScopedValue, with no extra plumbing required to pass that context into each subtask individually. This matters specifically because the alternative — manually capturing the relevant context values before forking each subtask and passing them in as explicit parameters or closure captures — works but reintroduces exactly the kind of boilerplate that motivated ThreadLocal-style implicit context propagation in the first place, except now multiplied across every individual fork() call in a structured-concurrency-based codebase. This inheritance is read-only and one-directional in the way that matters for safety: a subtask reading an inherited scoped value sees the binding exactly as the owner had it, but nothing about how scoped values work allows a subtask to set or change that value in a way that's visible back to the owner or to sibling subtasks, since there's no general mutation operation in the first place — each subtask either inherits the parent's binding as-is, or can establish its own further-nested binding (via its own where(...).run()/call()) that's visible only within that subtask's own further call chain, exactly mirroring the ordinary nested-rebinding behavior described above, just now happening across a fork() boundary into a separate virtual thread rather than within a single thread's own sequential call chain. This combination — Scoped Values for safe, immutable, automatically-propagated per-operation context, and StructuredTaskScope for safe, lexically-bounded concurrent subtask execution — is presented together deliberately in the JDK's own documentation and JEPs specifically because they were designed to complement each other: structured concurrency provides the safe mechanism for actually running concurrent subtasks, and Scoped Values provide the safe mechanism for those subtasks to access the same contextual data the forking code already had, without either mechanism requiring the kind of manual bookkeeping or mutation hazards that ExecutorService-based concurrency plus ThreadLocal-based context propagation had always required.
Java
// (Scoped Values finalized in Java 25 — JEP 506; Structured Concurrency
// remains a preview feature as of Java 25, requiring --enable-preview)

static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

void handleRequest(String requestId) {
    ScopedValue.where(REQUEST_ID, requestId).run(() -> {
        try (var scope = StructuredTaskScope.open()) {
            // Both subtasks below automatically INHERIT REQUEST_ID's binding from
            // this owner thread — no explicit parameter needed to pass it along:
            var userTask = scope.fork(() -> fetchUserData());
            var orderTask = scope.fork(() -> fetchOrderData());

            scope.join();
            combineResults(userTask.get(), orderTask.get());
        }
    });
}

String fetchUserData() {
    // Running on a DIFFERENT (virtual) thread than handleRequest's owner thread,
    // yet REQUEST_ID.get() here returns the SAME value the owner had when the
    // scope was opened — inherited automatically, no manual propagation needed:
    System.out.println("Fetching user data for request: " + REQUEST_ID.get());
    return "user-data";
}

// ── A subtask CAN establish its own further-nested binding, scoped only to itself
String fetchOrderData() {
    return ScopedValue.where(REQUEST_ID, REQUEST_ID.get() + "-orders-suffix")
        .call(() -> {
            // REQUEST_ID here is the rebound value, visible only within THIS nested call —
            // sibling subtask "fetchUserData" above is entirely unaffected by this rebinding,
            // and the owner thread's own original binding is unaffected too:
            return "orders for " + REQUEST_ID.get();
        });
}

Related Topics in Java 9 to 21 Features

Module System (JPMS)
The Java Platform Module System (JPMS), introduced in Java 9 under Project Jigsaw, adds a module as a new, higher-level unit of code organization above the package, allowing a JAR-like artifact to explicitly declare which packages it exports for use by other modules, which modules it depends on, and which packages it keeps entirely internal and inaccessible from outside — closing a long-standing gap in the classpath model where every public class in every JAR on the classpath was accessible to every other JAR, regardless of whether that access was intended. JPMS was also the mechanism used to decompose the JDK itself, which had grown into one monolithic rt.jar, into roughly 90 separate modules, enabling smaller custom runtime images and explicit internal-API encapsulation (notably restricting access to sun.* and other internal packages that many libraries had relied on despite warnings). This entry covers the classpath problems JPMS solves, module-info.java syntax and the requires/exports/opens directives, the distinction between the classpath and module path including automatic and unnamed modules, and jlink custom runtime images.
JShell
JShell, introduced in Java 9, is an interactive Read-Eval-Print Loop (REPL) bundled directly with the JDK, allowing Java statements, expressions, method, class, and variable declarations to be entered and evaluated one at a time at an interactive prompt, with immediate feedback — without requiring a class wrapper, a main method, explicit compilation, or a build tool, all of which Java had always required for even the smallest snippet of executable code before Java 9. This solved a long-standing friction point for learning, quick experimentation, and API exploration, where languages like Python or Ruby offered an immediate interactive prompt but Java required the full edit-compile-run cycle even to test a single line. This entry covers what problem JShell solves and how it differs from a script, core REPL usage including implicit variable creation and forward references, snippet management commands, and how to feed JShell external classes and files.
Collection Factory Methods
Collection factory methods — List.of(...), Set.of(...), and Map.of(...)/Map.ofEntries(...) — introduced in Java 9, provide a concise, standard way to create small, fixed-content, genuinely immutable collections in a single expression, replacing the previously common but verbose combination of Arrays.asList(...), Collections.unmodifiableList(...), and manual add() calls, none of which produced a collection that was both concise to write and fully immutable. This entry covers exactly what problem these factories solve compared to the older idioms, the specific immutability and null-rejection guarantees they provide (which are stricter than Collections.unmodifiableXxx), their behavior around duplicate keys/elements, and important caveats around mutability expectations and performance for very small collections.
Private Interface Methods
Private interface methods, introduced in Java 9, allow an interface to declare methods marked private — with a full method body — that are accessible only from within that same interface (from its default methods, its other private methods, and its private static methods), but completely invisible to implementing classes and to any code outside the interface. They close a code-duplication gap left after Java 8's default methods: when an interface has multiple default methods that share common logic, that shared logic previously either had to be duplicated in each default method or exposed as a public default method purely to enable reuse, polluting the interface's actual API surface with a method meant only for internal reuse. This entry covers the specific problem they solve, the distinction between private instance and private static interface methods, the rules governing what they can call and be called from, and how they interact with the broader default-method design Java established in version 8.