Scoped Values in Java

Naveen AnamaneniUpdated · 4 min read

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

MethodDescription
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.

Scoped.java▶ Try it
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());
    }
}
Output
order placed by Navika
order placed by Srihitha
order placed by anonymous
outer: Advitha
inner: admin
outer again: Advitha
bound outside? false

Explanation: 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

ThreadLocalScopedValue
Can be changedYes, set any timeNo; immutable within a scope (rebind in a nested scope)
LifetimeUntil remove() or the thread diesExactly the run/call scope
Leak risk in poolsYes, if remove() is forgottenNone
Cost with many virtual threadsA copy per threadCheap to bind and read
Shared with child tasksOnly with InheritableThreadLocal (copied)Automatically with structured concurrency
StatusSince Java 1.2Final 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

  1. Easy: Bind a REQUEST_ID scoped value and print it from a method three calls deep.
  2. Harder: Bind a LOCALE scoped value ("en" or "fr") and write a greet(String name) method that says "Hello" or "Bonjour" depending on it.

Quick check: Scoped values

4 questions
1In which version did scoped values become final?
2How long does a scoped binding last?
3Can code inside the scope change the value?
4What does KEY.get() do when nothing is bound?

Key Takeaways

Was this tutorial helpful?

Lesson 431 of 482