Scoped Values in Java
A scoped value, final in Java 25 (JEP 506), is a way to share an immutable value with all the code that a thread calls, for a limited scope, without passing it as a parameter through every method. You bind a value with ScopedValue.where(KEY, value) and run code with run or call; inside that code, any method can read it with KEY.get(). When the code finishes, the binding disappears automatically. Scoped values are a safer, cheaper replacement for many uses of ThreadLocal, especially with virtual threads. In this lesson, you will learn how to use them.
Methods
| Method | Description |
|---|---|
ScopedValue.newInstance() | It is used to create a key, usually a static final field. |
ScopedValue.where(key, value) | It prepares a binding of a value to a key. |
.run(runnable) / .call(callable) | It runs code with the binding in place. |
key.get() | It returns the bound value; throws if unbound. |
key.isBound() | It reports whether a value is bound. |
key.orElse(other) | It returns the bound value, or other. |
Example: Passing the Current User Without Parameters
Let's see an example where deep methods read the current user without it being passed down.
public class Scoped {
static final ScopedValue<String> USER = ScopedValue.newInstance();
static void handleRequest() {
placeOrder();
}
static void placeOrder() {
audit("order placed");
}
static void audit(String action) {
System.out.println(action + " by " + USER.orElse("anonymous"));
}
public static void main(String[] args) {
ScopedValue.where(USER, "Navika").run(Scoped::handleRequest);
ScopedValue.where(USER, "Srihitha").run(Scoped::handleRequest);
handleRequest();
ScopedValue.where(USER, "Advitha").run(() -> {
System.out.println("outer: " + USER.get());
ScopedValue.where(USER, "admin").run(() ->
System.out.println("inner: " + USER.get()));
System.out.println("outer again: " + USER.get());
});
System.out.println("bound outside? " + USER.isBound());
}
}order placed by Navika
order placed by Srihitha
order placed by anonymous
outer: Advitha
inner: admin
outer again: Advitha
bound outside? falseExplanation: where(USER, "Navika").run(...) binds the user for exactly the duration of handleRequest, so audit, two calls deeper, can read it. Outside any run, the value is unbound and orElse supplies "anonymous". A nested where rebinds the value for the inner scope only; when the inner scope ends, the outer value is back. After all scopes end, nothing remains bound, so nothing can leak.
Scoped Values vs ThreadLocal
ThreadLocal | ScopedValue | |
|---|---|---|
| Can be changed | Yes, set any time | No; immutable within a scope (rebind in a nested scope) |
| Lifetime | Until remove() or the thread dies | Exactly the run/call scope |
| Leak risk in pools | Yes, if remove() is forgotten | None |
| Cost with many virtual threads | A copy per thread | Cheap to bind and read |
| Shared with child tasks | Only with InheritableThreadLocal (copied) | Automatically with structured concurrency |
| Status | Since Java 1.2 | Final in Java 25 |
Common Mistakes
Calling KEY.get() outside any binding. It throws NoSuchElementException; use isBound() or orElse.
Trying to set a new value inside a scope. Bind a new value in a nested where(...).run(...) instead.
Making the key non-static or non-final. Keys are meant to be shared constants.
Practice
- Easy: Bind a
REQUEST_IDscoped value and print it from a method three calls deep. - Harder: Bind a
LOCALEscoped value ("en" or "fr") and write agreet(String name)method that says "Hello" or "Bonjour" depending on it.
Key Takeaways
Was this tutorial helpful?