Java 25 Scoped Values: A Cleaner Way to Pass Context
- Published on
- Authors
- Name
- Spaghetti Code Jungle
- @spagcodejungle

Passing context through an application can get messy fast.
A request ID starts in your controller.
Then it moves into a service.
Then into another service.
Then into a logger.
Then into a background task.
Before long, your method signatures are carrying values that are not really business inputs. They are just context.
Java 25 gives us a better tool for this problem: Scoped Values.
Scoped Values are finalized in Java 25 through JEP 506. They let a method share data with its callees, and with child threads, for a limited and clearly defined execution scope. They are designed to be easier to reason about than ThreadLocal, especially when used with virtual threads and structured concurrency.
The Problem: Context Gets Noisy
Imagine you need a requestId for logging:
void handleRequest(String requestId) {
validate(requestId);
processOrder(requestId);
sendResponse(requestId);
}
At first, this looks fine.
But then more context appears:
void processOrder(
String requestId,
String tenantId,
User user,
Locale locale,
String traceId
) {
// business logic
}
Now your method signature is doing too much.
Some of those values are not part of the core business operation. They are surrounding context.
That is where Scoped Values help.
What Are Scoped Values?
A ScopedValue is a value that is bound for a limited block of execution.
Code inside that block can access the value. Code outside that block cannot.
The Oracle Java 25 API docs describe this as a value bound for the bounded period of a method execution. When the original operation completes, the value becomes unbound again.
Here is a simple example:
import java.lang.ScopedValue;
public class RequestContextExample {
private static final ScopedValue<String> REQUEST_ID =
ScopedValue.newInstance();
public static void main(String[] args) {
ScopedValue.where(REQUEST_ID, "req-123")
.run(() -> {
handleRequest();
});
}
static void handleRequest() {
logMessage("Processing request");
}
static void logMessage(String message) {
System.out.println(REQUEST_ID.get() + ": " + message);
}
}
The key idea is simple:
You bind the value once. You use it inside the scope. Java removes the binding when the scope ends.
Why Not Just Use ThreadLocal?
ThreadLocal has been used for years to store request context, security context, logging metadata, and similar values.
But ThreadLocal can be easy to misuse.
A value can be set in one place, forgotten in another, and accidentally kept around longer than intended. That makes the code harder to reason about.
Scoped Values make the lifetime more obvious.
Instead of saying:
“This value belongs to the thread.”
Scoped Values say:
“This value belongs to this execution scope.”
That is a much cleaner mental model.
OpenJDK also describes Scoped Values as having lower space and time costs than thread-local variables, especially when used with virtual threads and structured concurrency.
The Best Use Cases
Scoped Values are a good fit for context such as:
- request IDs
- tenant IDs
- user identity
- locale
- tracing metadata
- logging context
- security context
They are not a replacement for every method parameter.
If a value is essential business input, pass it explicitly.
If a value is surrounding context, a Scoped Value may be a better fit.
Important Detail: Treat the Data as Immutable
Scoped Values are designed for sharing immutable data.
That does not magically make every object immutable. If you bind a mutable object, Java will not freeze it for you.
So the safest pattern is to bind simple values, records, or objects that your code treats as immutable.
For example:
record RequestContext(
String requestId,
String tenantId,
String traceId
) {}
Then bind the whole context:
private static final ScopedValue<RequestContext> CONTEXT =
ScopedValue.newInstance();
ScopedValue.where(
CONTEXT,
new RequestContext("req-123", "tenant-a", "trace-999")
).run(() -> {
handleRequest();
});
This keeps related context together without passing it through every method.
Why Scoped Values Matter More With Concurrency
Scoped Values are especially interesting when combined with structured concurrency.
With StructuredTaskScope, scoped value bindings can be inherited by child tasks started inside the scope. That means context can flow into subtasks in a controlled way.
That is useful for modern backend systems where one request might trigger multiple concurrent operations:
ScopedValue.where(CONTEXT, requestContext).run(() -> {
// child tasks can inherit the scoped context
});
This makes context propagation feel less like plumbing and more like part of the structure of the program.
Java 25 Migration Note
If you tried Scoped Values in earlier preview versions, there is one Java 25 detail to know:
ScopedValue.orElse(null) is no longer allowed.
JEP 506 notes this as part of the finalization of the API in Java 25.
When Should You Use Scoped Values?
Use Scoped Values when:
- the data is contextual
- the data should be read-only
- the value has a clear lifetime
- many methods need access to it
- passing it everywhere would make the code noisy
Avoid Scoped Values when:
- the value is core business input
- the data needs to be mutated
- the scope is unclear
- explicit parameters would make the code easier to understand
A good rule of thumb:
If the method cannot do its job without the value, pass it as a parameter. If the value describes the environment around the job, consider a Scoped Value.
Final Thoughts
Scoped Values are not the loudest feature in Java 25.
They do not introduce flashy syntax. They do not rewrite how Java works. They do not make every app magically faster.
But they solve a real design problem.
They give Java developers a cleaner way to pass context through code without turning every method signature into a suitcase.
For junior developers, Scoped Values are a great lesson in code clarity. For senior developers, they are a useful tool for designing cleaner frameworks, services, and concurrent systems.
Java 25 does not just make Java faster.
It makes Java feel more intentional.