Java 21 introduces Scoped Values, a powerful new feature designed to safely and efficiently share data across methods and virtual threads, particularly in functional and concurrent code. Combined with lambda expressions, scoped values offer a cleaner, safer alternative to ThreadLocal
.
In this tutorial, you'll learn how scoped values work, how to integrate them with lambdas and functional interfaces, and how they improve context propagation in modern Java applications.
🔍 What Are Scoped Values?
Scoped values are a lightweight mechanism to share immutable data within a dynamic scope, especially across virtual threads. Unlike ThreadLocal
, they:
- Are immutable
- Do not leak across thread boundaries
- Work seamlessly with structured concurrency
- Have predictable lifetimes and cleanup
static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
💡 Why Scoped Values + Lambdas?
Lambdas are widely used for async execution, functional pipelines, and callbacks. Scoped values ensure data (e.g., request IDs, auth tokens, tenant context) is available across these lambdas without polluting method signatures or relying on brittle static fields.
✅ How to Use Scoped Values with Lambdas
1. Declare the ScopedValue
static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
2. Bind a value in a specific scope
ScopedValue.where(USER_ID, "user-123").run(() -> {
processRequest(); // Any method/lambda here can access USER_ID.get()
});
3. Access the value in downstream lambdas
public static void processRequest() {
Runnable task = () -> {
String userId = USER_ID.get(); // safe access
System.out.println("Running task for: " + userId);
};
new Thread(task).start(); // Works with virtual threads too
}
🔧 Scoped Values in Functional Pipelines
Scoped values can cleanly inject context into stream pipelines, logging, or async flows.
ScopedValue.where(USER_ID, "u-789").run(() -> {
List<String> items = List.of("A", "B", "C");
items.stream()
.map(item -> "User " + USER_ID.get() + " processed " + item)
.forEach(System.out::println);
});
🧪 Thread Safety and Lambdas
- Scoped values are isolated to a scope, making them ideal for multi-threaded lambdas.
- Avoid sharing mutable state or relying on static fields when using lambdas in concurrent code.
📦 Scoped Values vs ThreadLocal
Feature | ThreadLocal | ScopedValue (Java 21) |
---|---|---|
Mutability | Mutable | Immutable |
Lifecycle | Must be cleared manually | Automatic, scoped |
Works with Virtual Threads | ❌ Bug-prone | ✅ Safe and preferred |
Cleanup guarantees | Manual, error-prone | Automatic after scope ends |
Memory safety | Leaky | Predictable |
🔄 Real-World Use Case: Tenant Context Propagation
static final ScopedValue<String> TENANT_ID = ScopedValue.newInstance();
public static void handleRequest(String tenantId) {
ScopedValue.where(TENANT_ID, tenantId).run(() -> {
doBusinessLogic(() -> {
System.out.println("Tenant: " + TENANT_ID.get());
});
});
}
static void doBusinessLogic(Runnable task) {
new Thread(task).start(); // Virtual thread recommended
}
📌 What's New in Java Versions?
Java 8
- Lambdas,
java.util.function
, Streams, CompletableFuture
Java 11
var
in lambda parameters, stream enhancements
Java 17
- Sealed interfaces, enhanced switch expressions
Java 21
- ✅ Scoped Values
- ✅ Virtual Threads (Project Loom)
- ✅ Structured Concurrency
- ✅ Enhanced compatibility with lambdas and functional interfaces
⚠️ Common Pitfalls
- ❌ Mutating data in lambdas using scoped values (they are meant to be immutable)
- ❌ Using scoped values across unrelated threads or outside of
ScopedValue.where(...)
- ❌ Mixing ThreadLocal and ScopedValue in the same codebase for the same purpose
🧱 Functional Patterns Enhanced by Scoped Values
- Command Pattern: Lambdas injected with scoped context
- Observer Pattern: Event listeners with shared scoped metadata
- Strategy Pattern: Runtime logic selected using scoped flags or values
✅ Conclusion and Key Takeaways
- Scoped values offer a safer, cleaner alternative to ThreadLocal
- They work beautifully with lambdas, streams, and functional pipelines
- Their design aligns with modern virtual-thread-first concurrency
- Use them for passing context like user IDs, auth, tracing, and more
❓ FAQ
1. Can I modify a scoped value after it's bound?
No. Scoped values are immutable once set in their dynamic scope.
2. Are scoped values accessible in nested lambdas?
Yes, as long as they’re within the same ScopedValue.where()
scope.
3. How do scoped values differ from ThreadLocal in memory usage?
Scoped values are safer and automatically cleaned, avoiding leaks common with ThreadLocal.
4. Do scoped values work in executors or parallel streams?
Only if the scoped context is preserved—recommended for structured concurrency and virtual threads.
5. Can scoped values be used with Spring?
Yes, especially in reactive pipelines or AOP wrappers, but integration patterns are evolving.
6. Is ScopedValue production-ready?
Yes. As of Java 21, it's a stable, officially supported API.
7. Can scoped values carry complex objects?
Yes. You can pass any object type, just ensure it’s immutable or read-only.
8. Are scoped values garbage collected?
Yes, once the scope ends, references are released.
9. Can I nest ScopedValue scopes?
Yes. Inner scopes can override outer scoped values.
10. Do ScopedValue instances need to be static?
Typically yes, as they represent keys for scoped values and should be reused.