A ScopedValue binds a value to the current execution scope and restores the previous binding when that scope returns or throws.
Java 25 scoped values: request context with a bounded lifetime
Java 25+. The program uses JDK classes and requires no preview flags.
Pass one-way context
A reporting operation needs a request identifier deep inside callbacks. Keeping an explicitly passed context object is still a valid choice. A scoped value fits when intermediate methods do not need the value but selected downstream code does. Keep its key private so unrelated code cannot read or rebind it merely because it knows a field name.
The binding is not a writable thread-local slot that remains until remove is called. A nested binding temporarily shadows the outer value; leaving the nested operation restores the outer binding even when the nested code throws. The program checks that path instead of assuming every callback returns normally.
Binding lifetime does not freeze an object. This example binds an immutable String. A mutable request object can still be changed through another reference. Ordinary newly created threads do not automatically receive this binding; structured sharing has its own API rules. No preview structured-concurrency API is used in this lesson.
Make missing context explicit
Calling get outside a binding is a misuse unless the API contract permits an optional context and checks it deliberately. The operation should not silently invent a request identifier just because a caller forgot to establish its scope. The final isBound check proves cleanup after the operation.
Working program
public class ReportRequestScope {
private static final ScopedValue<String> REQUEST=ScopedValue.newInstance();
public static void main(String[] args)throws Exception{
ScopedValue.where(REQUEST,"REQ-51").call(()->{
System.out.println(REQUEST.get());
try{ScopedValue.where(REQUEST,"AUDIT-51").run(()->{throw new IllegalStateException("audit failed");});}
catch(IllegalStateException failed){System.out.println(REQUEST.get());}
Thread child=Thread.ofVirtual().start(()->System.out.println(REQUEST.isBound()));
child.join();return null;
});
System.out.println(REQUEST.isBound());
}
}Output
REQ-51
REQ-51
false
falseCosts and boundaries
Binding and lookup do not traverse application collections. Lookup cost depends on the scope implementation and its cache; this program makes no nanosecond claim. Referenced context remains reachable while the binding needs it. Java 25 finalizes this API; earlier preview versions are not the baseline used here.
Common Mistakes
- A bounded binding does not make a mutable payload immutable.
- A raw child thread does not imply scoped inheritance.
- Do not expose the key as a public application-wide permission.
