Scoped Values vs ThreadLocal
Share immutable data down a call chain without parameters and without cleanup bugs.
static final ScopedValue<String> USER = ScopedValue.newInstance();
void main() {
ScopedValue.where(USER, "alice").run(() -> handle("first request"));
ScopedValue.where(USER, "bob").run(() -> handle("second request"));
IO.println("Outside any scope, bound? " + USER.isBound());
}
void handle(String request) {
IO.println(USER.get() + " -> " + request);
audit();
}
void audit() {
// No parameter passing: the value is visible all the way down the call chain.
IO.println("audit: request made by " + USER.get());
} Run with JDK 25: java ScopedValues.java
A ScopedValue holds a value that is visible to a method and everything it calls, for a bounded time. When the scope ends, the binding disappears on its own.
ScopedValue vs ThreadLocal
ThreadLocal | ScopedValue | |
|---|---|---|
| Mutable | Yes (set) | No, rebind in a nested scope instead |
| Lifetime | Until you call remove() | Only inside the run or call block |
| Forgotten cleanup leaks data | Yes | No |
Output
alice -> first request
audit: request made by alice
bob -> second request
audit: request made by bob
Outside any scope, bound? false
Takeaway
If you pass a user, tenant or request id through five method signatures just so the sixth can read it, a ScopedValue is the cleaner tool. Keep using ThreadLocal only where you truly need a mutable per-thread slot.