Java 25 Scoped Values Tutorial with Examples
Introduction
Applications often need to make some information available to all the code that handles a request - the current user, a transaction, a request ID for logging - without passing it as a parameter through every method in between. Since Java 1.2, the tool for this has been
OUTPUT of the above code
Explanation of the code
Rebinding in a nested scope
Although a binding cannot be changed, code can rebind the same scoped value for the call it makes - for example to run part of a request with elevated rights. The new value is visible only inside the nested call, and the original binding is back when it returns -
OUTPUT of the above code
Scoped values and threads
Each thread has its own bindings: a scoped value bound in one thread is not visible in another, just like a
OUTPUT of the above code
Scoped values were designed to be inherited by child threads cheaply when work is split up with structured concurrency (
Scoped values vs ThreadLocal
ThreadLocal. But ThreadLocal has problems: any code can change its value at any time, the value lives until someone remembers to call remove() - a common source of leaks and of data leaking between requests in thread pools - and child threads which inherit it get expensive copies. With virtual threadsClick to read: virtual threads, where an application may run millions of threads, these costs matter even more. Scoped values, incubated in Java 20, previewed from Java 21 to 24 and final in Java 25Click to read tutorial: Overview of New Features in Java 25 (JEP 506), are the modern alternative. This tutorial explains scoped values with examples - binding and reading a value, rebinding it, how they compare with ThreadLocal, and how they behave with threads.
Binding and reading a scoped value
A ScopedValue is usually a static final field. A value is bound to it for the duration of a method call with ScopedValue.where(key, value).run(...), and any code called from there - however deeply - reads it with get(). When run() returns, the binding is gone -
Sharing the current user with code called deep in a request
//ScopedValueDemo.java
public class ScopedValueDemo {
static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();
// Framework code: binds the user for the duration of the request
static void handleRequest(String user, Runnable handler) {
ScopedValue.where(CURRENT_USER, user).run(handler);
}
// Application code: no user parameter anywhere
static void showOrders() { audit("viewed orders"); }
static void audit(String action) {
System.out.println("[" + CURRENT_USER.get() + "] " + action); // read the bound value
}
public static void main(String[] args) {
handleRequest("tom", ScopedValueDemo::showOrders);
handleRequest("harry", ScopedValueDemo::showOrders);
System.out.println("Bound outside a request? " + CURRENT_USER.isBound());
System.out.println("With a default: " + CURRENT_USER.orElse("anonymous"));
try {
CURRENT_USER.get();
} catch (java.util.NoSuchElementException e) {
System.out.println("get() when unbound -> NoSuchElementException");
}
}
}
[tom] viewed orders [harry] viewed orders Bound outside a request? false With a default: anonymous get() when unbound -> NoSuchElementException
ScopedValue.where(CURRENT_USER, user)creates a binding, and.run(handler)runs the handler with it.audit(), two calls down, reads the value without it being passed as a parameter.call()works likerun()for code which returns a result.- The binding has a clear lifetime: exactly the duration of
run(). There is noremove()to forget, so values cannot leak from one request into the next. - Outside any binding,
isBound()is false,orElse()returns a default, andget()throwsNoSuchElementException. - A scoped value is immutable within its scope: there is no
set(), so no code in the call chain can change what code further up bound.
A nested binding for part of the work
//RebindDemo.java
public class RebindDemo {
static final ScopedValue<String> ROLE = ScopedValue.newInstance();
static void log(String step) { System.out.println(step + " as " + ROLE.get()); }
public static void main(String[] args) {
ScopedValue.where(ROLE, "user").run(() -> {
log("Read profile");
ScopedValue.where(ROLE, "admin").run(() -> log("Write audit record")); // nested rebinding
log("Render page"); // back to "user"
});
}
}
Read profile as user Write audit record as admin Render page as user
ThreadLocal. The program below binds a request ID in many virtual threads at once, and each sees only its own -
Independent bindings in concurrent virtual threads
//ThreadsDemo.java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadsDemo {
static final ScopedValue<Integer> REQUEST_ID = ScopedValue.newInstance();
static void work(int expected, AtomicInteger mismatches) throws InterruptedException {
Thread.sleep(10); // other threads run meanwhile
if (REQUEST_ID.get() != expected) mismatches.incrementAndGet();
}
public static void main(String[] args) {
AtomicInteger mismatches = new AtomicInteger();
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int id = i;
executor.submit(() -> ScopedValue.where(REQUEST_ID, id).call(() -> { work(id, mismatches); return null; }));
}
}
System.out.println("10,000 concurrent requests, values seen by the wrong request: " + mismatches.get());
}
}
10,000 concurrent requests, values seen by the wrong request: 0
StructuredTaskScope, still a preview API in Java 25): subtasks forked in a scope automatically see their parent's bindings, without copying. Since bindings are immutable, sharing them is safe.
| ThreadLocal | ScopedValue | |
|---|---|---|
| Mutability | Any code can set() a new value | Immutable within a binding; only rebinding for a nested call |
| Lifetime | Until remove() or the thread ends - easy to leak | Exactly the duration of run() or call() |
| Child threads | InheritableThreadLocal copies values | Shared without copying in structured concurrency |
| Best for | Per-thread caches of expensive objects | Passing context (user, transaction, request ID) down a call chain |
ThreadLocal is not deprecated and remains the right tool when code genuinely needs a mutable, per-thread value. For one-way transmission of context from a caller to its callees, scoped values are simpler, safer and cheaper.
Summary
In this tutorial we saw how scoped values, final in Java 25, share immutable data with the methods a thread calls, without passing it as a parameter. We bound a value with ScopedValue.where().run() and read it deep in the call chain with get(), used isBound() and orElse() outside a binding, rebound a value for a nested call, and saw that each of 10,000 concurrent virtual threads sees only its own binding. Lastly, we compared scoped values with ThreadLocal. Next, read about compact object headersClick to read tutorial: Java 25 Compact Object Headers Explained.Continue Reading Java 22 to 25 Tutorials
Overview of New Features in Java 25Click to read tutorial: Overview of New Features in Java 25 Java 25 Compact Source Files and Instance main MethodsClick to read tutorial: Java 25 Compact Source Files and Instance main Methods Java 25 Module Import Declarations with ExamplesClick to read tutorial: Java 25 Module Import Declarations with Examples Java 25 Flexible Constructor Bodies with ExamplesClick to read tutorial: Java 25 Flexible Constructor Bodies with Examples Java 25 Compact Object Headers ExplainedClick to read tutorial: Java 25 Compact Object Headers Explained Java 25 Key Derivation Function (KDF) API TutorialClick to read tutorial: Java 25 Key Derivation Function (KDF) API Tutorial Overview of New Features in Java 22Click to read tutorial: Overview of New Features in Java 22 Overview of New Features in Java 23Click to read tutorial: Overview of New Features in Java 23 Overview of New Features in Java 24Click to read tutorial: Overview of New Features in Java 24
Overview of New Features in Java 25Click to read tutorial: Overview of New Features in Java 25 Java 25 Compact Source Files and Instance main MethodsClick to read tutorial: Java 25 Compact Source Files and Instance main Methods Java 25 Module Import Declarations with ExamplesClick to read tutorial: Java 25 Module Import Declarations with Examples Java 25 Flexible Constructor Bodies with ExamplesClick to read tutorial: Java 25 Flexible Constructor Bodies with Examples Java 25 Compact Object Headers ExplainedClick to read tutorial: Java 25 Compact Object Headers Explained Java 25 Key Derivation Function (KDF) API TutorialClick to read tutorial: Java 25 Key Derivation Function (KDF) API Tutorial Overview of New Features in Java 22Click to read tutorial: Overview of New Features in Java 22 Overview of New Features in Java 23Click to read tutorial: Overview of New Features in Java 23 Overview of New Features in Java 24Click to read tutorial: Overview of New Features in Java 24