Core Idea
Scoped Values provide a way to share immutable data within a defined scope and across threads in a structured manner. Think of them as "contextual variables" that are automatically available to all code running within a specific boundary.
The Mental Model
Imagine you're in a building:
- Scoped Value: Like the lighting in a room
- Scope: The room itself
- Value: The specific light setting (brightness, color)
- Code: People in the room
Once you set the lights in a room (bind the value), everyone in that room can see with those lights (access the value). When you leave the room, you can't see those lights anymore. Different rooms can have different lighting settings.
Key Characteristics
1. Immutability
final ScopedValue<String> USER = ScopedValue.newInstance();
// Once set, it cannot be changed within the scope
ScopedValue.where(USER, "Alice").run(() -> {
System.out.println(USER.get()); // "Alice"
// USER.set("Bob"); // NOT ALLOWED - would cause error
});
2. Structured Scoping
ScopedValue.where(USER, "Alice").run(() -> {
// This is the "scope" where USER is available
processRequest();
});
// Outside here, USER is not accessible
3. Thread Safety & Inheritance
ScopedValue.where(user, "Hakim").run(() -> {
try (var scope = new StructuredTaskScope<String>()) {
// This will properly inherit the scoped value
scope.fork(() -> {
System.out.println("Child thread: " + user.get());
return "done";
});
scope.join(); // Wait for the child thread to complete
} catch (InterruptedException e) {
e.printStackTrace();
}
});
How It Works: The Plumbing Metaphor
Think of Scoped Values like water pipes in a building:
- Declaring a Scoped Value: Installing a pipe system
- Binding a value: Turning on the water with specific pressure/temperature
- Running a scope: Opening a room where the water flows
- Accessing the value: Turning on a faucet in that room
// Install the pipe system
final ScopedValue<String> WATER_TEMP = ScopedValue.newInstance();
// Turn on hot water in the bathroom
ScopedValue.where(WATER_TEMP, "hot").run(() -> {
takeShower(); // Faucets get hot water
});
// Turn on cold water in the kitchen
ScopedValue.where(WATER_TEMP, "cold").run(() -> {
fillWaterBottle(); // Faucets get cold water
});
Real-world Analogies
1. Movie Theater Analogy
final ScopedValue<String> MOVIE = ScopedValue.newInstance();
// Theater 1: Action movie
ScopedValue.where(MOVIE, "Action").run(() -> {
watchMovie(); // Everyone here sees the action movie
});
// Theater 2: Comedy movie
ScopedValue.where(MOVIE, "Comedy").run(() -> {
watchMovie(); // Everyone here sees the comedy
});
2. Construction Site Analogy
final ScopedValue<String> SAFETY_LEVEL = ScopedValue.newInstance();
// High-risk area
ScopedValue.where(SAFETY_LEVEL, "extreme").run(() -> {
work(); // All workers follow extreme safety protocols
});
// Low-risk area
ScopedValue.where(SAFETY_LEVEL, "normal").run(() -> {
work(); // Normal safety protocols apply
});
The Magic: Automatic Propagation
The most powerful aspect is how values automatically propagate:
final ScopedValue<String> CONTEXT = ScopedValue.newInstance();
ScopedValue.where(CONTEXT, "important").run(() -> {
method1(); // CONTEXT is available here
});
void method1() {
method2(); // CONTEXT is available here too
}
void method2() {
System.out.println(CONTEXT.get()); // "important" - no need to pass parameters!
}
void method3() {
ScopedValue.where(user, "Hakim").run(() -> {
try (var scope = new StructuredTaskScope<String>()) {
// This will properly inherit the scoped value
scope.fork(() -> {
System.out.println("Child thread: " + user.get());
return "done";
});
scope.join(); // Wait for the child thread to complete
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
Why This Matters: Solving Real Problems
Before (ThreadLocal problems):
// ThreadLocal - fragile with virtual threads
ThreadLocal<String> userContext = new ThreadLocal<>();
void processRequest() {
userContext.set("Alice");
try {
// Works but doesn't play well with virtual threads
Thread.newVirtualThread(this::doWork).start();
} finally {
userContext.remove(); // Easy to forget, causes memory leaks
}
}
After (Scoped Values solution):
// ScopedValue - safe and structured
final ScopedValue<String> USER_CONTEXT = ScopedValue.newInstance();
void processRequest() {
ScopedValue.where(USER_CONTEXT, "Alice").run(() -> {
ScopedValue.where(user, "Hakim").run(() -> {
try (var scope = new StructuredTaskScope<String>()) {
// This will properly inherit the scoped value
scope.fork(() -> {
doWork();
return "done";
});
scope.join(); // Wait for the child thread to complete
} catch (InterruptedException e) {
e.printStackTrace();
}
});
});
// Automatically cleaned up here
}
The Big Picture
Scoped Values solve several problems at once:
- Structured context propagation - Values flow naturally through call stacks
- Thread safety - Immutable by design, safe for concurrent access
- Virtual thread compatibility - Designed for modern Java concurrency
- Memory safety - Automatic cleanup prevents memory leaks
- Clean code - Eliminates parameter passing for contextual data
They're particularly valuable for:
- Web request contexts
- Transaction management
- User session information
- Tracing and logging
- Feature flag context
- Security context propagation
Top comments (0)