Scoped Values in Java

Use scoped values to share request-scoped context across modern Java concurrency without the lifetime problems of ThreadLocal.

Scoped values let a caller bind read-only context for a bounded execution scope. Code inside that scope can read the value, but the binding ends when the scope ends.

That sounds simple, but it is a significant design improvement over long-lived ThreadLocal usage. As of JDK 25, scoped values are a permanent Java feature.

Why They Matter

Many systems need contextual data that should follow a request through several layers:

  • request IDs
  • tenant IDs
  • security context
  • locale or policy context

Passing that data explicitly through every method is often best. But sometimes it creates plumbing noise that obscures the business logic. Older Java commonly solved that problem with ThreadLocal, which introduced a different risk: the data lived on the thread rather than on the logical operation.

Scoped values move the focus back to operation lifetime.

A Simple Example

 1import java.lang.ScopedValue;
 2
 3public class AuditContext {
 4    private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
 5
 6    static void handleRequest(String requestId) {
 7        ScopedValue.where(REQUEST_ID, requestId).run(() -> {
 8            serviceLayer();
 9        });
10    }
11
12    static void serviceLayer() {
13        logger.info("request={}", REQUEST_ID.get());
14    }
15}

The important design property is not just access. It is bounded lifetime. Once the scope exits, the binding is gone.

Scoped Values Versus ThreadLocal

ConcernThreadLocalscoped values
lifetimeattached to the thread until clearedattached to an explicit dynamic scope
mutabilityusually mutable state by conventiondesigned for read-only contextual sharing
leakage riskeasy to forget cleanup in pooled threadslower because the binding ends with the scope
fit with virtual threadsworks, but can still blur ownershipbetter matches request-style lifetime

This is why scoped values are especially relevant in the Loom era. Cheap threads are helpful, but clear lifetime is what makes concurrent code easier to reason about.

Good Uses

Scoped values fit best for metadata that is:

  • request-scoped
  • read-only
  • needed across multiple internal layers
  • awkward to pass through every intermediate helper

Good examples:

  • trace IDs
  • tenant identity
  • policy context
  • feature flags derived at request entry

Bad Uses

Do not use scoped values as an excuse to hide core business inputs.

Bad examples:

  • primary domain data that should be explicit parameters
  • mutable caches
  • service dependencies
  • state that outlives a single operation

If a value is important to understanding what a method does, passing it explicitly is often still better.

Why They Matter More With Structured Work

Scoped values and structured concurrency complement each other:

  • structured concurrency defines which tasks belong to the operation
  • scoped values define which context belongs to that operation

That combination makes it easier to review request-scoped concurrency without relying on ambient mutable state.

Practical Takeaway

Scoped values matter because they let Java share contextual data with explicit lifetime. They are not a replacement for normal parameters, but they are often a better replacement for ThreadLocal in modern concurrent Java systems.

Revised on Thursday, April 23, 2026