The simplest way to propagate context, logging and tracing in Java 21+ with Spring and virtual threads.
ScopeFlow is an open-source library that simplifies execution context propagation across technical and business layers in modern Java applications. It bridges the gap between logging (MDC), observability (Micrometer, OpenTelemetry), and modern concurrency (virtual threads, ScopedValue).
MDC.put("request.id", "abc-123");
executor.submit(() -> {
MDC.get("request.id"); // null! Context is lost across threads.
});try (Scope scope = scopeFlow.open("request", Map.of("request.id", "abc-123"))) {
executor.submit(scopeFlow.wrap(() -> {
MDC.get("request.id"); // "abc-123" — context travels automatically!
}));
}ScopeFlow provides a single scope-based abstraction that:
- ✅ Creates, enriches, and closes context scopes via
try-with-resources - ✅ Auto-propagates to SLF4J MDC for correlated logging
- ✅ Wraps tasks for cross-thread context propagation
- ✅ Integrates with Spring Boot via a zero-config starter
- ✅ Bridges to Micrometer (Reactor) and OpenTelemetry (Baggage)
- ✅ Supports virtual threads out of the box
- ✅ Provides ScopedValue & StructuredTaskScope integration (Java 23+ preview)
<dependency>
<groupId>io.scopeflow</groupId>
<artifactId>scopeflow-spring-boot-starter</artifactId>
<version>0.1.0</version>
</dependency>@RestController
public class OrderController {
@Autowired
private ScopeFlow scopeFlow;
@PostMapping("/orders")
public Order createOrder(@RequestBody OrderRequest req) {
// MVC interceptor already opened a scope with request.id, http.method, http.path
try (Scope scope = scopeFlow.open("order.create",
Map.of("customer.id", req.customerId()))) {
log.info("Creating order");
// Log: [req=abc-123] [customer.id=CUST-1] Creating order
return orderService.create(req);
}
}
}<dependency>
<groupId>io.scopeflow</groupId>
<artifactId>scopeflow-core</artifactId>
<version>0.1.0</version>
</dependency>
<dependency>
<groupId>io.scopeflow</groupId>
<artifactId>scopeflow-mdc</artifactId>
<version>0.1.0</version>
</dependency>ScopeFlow scopeFlow = ScopeFlowBuilder.create()
.propagator(new MdcPropagator(MdcKeyPolicy.allowAll()))
.build();
try (Scope scope = scopeFlow.open("order.process",
Map.of("order.id", "ORD-123"))) {
log.info("Processing"); // MDC has order.id=ORD-123
executor.submit(scopeFlow.wrap(() -> {
log.info("Async work"); // MDC still has order.id=ORD-123!
}));
}<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.github.FirstOnDie</groupId>
<artifactId>scopeflow-spring-boot-starter</artifactId>
<version>v0.1.0</version>
</dependency>| Module | Description | Status |
|---|---|---|
scopeflow-core |
Core API, scope lifecycle, context wrappers | ✅ Ready |
scopeflow-mdc |
SLF4J MDC bridge with save/restore stack | ✅ Ready |
scopeflow-micrometer |
Micrometer Context Propagation (Reactor) | ✅ Ready |
scopeflow-otel |
OpenTelemetry Baggage bridge | ✅ Ready |
scopeflow-scoped |
ScopedValue + StructuredTaskScope (Java 23+ preview) | ✅ Ready |
scopeflow-spring-boot-autoconfigure |
Spring Boot auto-configuration | ✅ Ready |
scopeflow-spring-boot-starter |
Single-dependency starter | ✅ Ready |
scopeflow-bom |
Bill of Materials (version alignment) | ✅ Ready |
Scopes nest naturally. Child scopes inherit parent values and restore them on close:
try (Scope request = scopeFlow.open("http.request",
Map.of("request.id", "abc-123"))) {
try (Scope order = scopeFlow.open("order.process",
Map.of("order.id", "ORD-456"))) {
// context has both request.id and order.id
}
// only request.id remains
}
// context is empty// Wrap individual tasks
executor.submit(scopeFlow.wrap(() -> { /* context here */ }));
// Or wrap the entire executor
ExecutorService wrapped = scopeFlow.wrapExecutorService(executor);
wrapped.submit(() -> { /* context here too */ });@Async
public void sendEmail(String orderId) {
log.info("Sending email"); // MDC has request.id from the calling thread
}ScopeFlow scopeFlow = ScopeFlowBuilder.create()
.propagator(new MdcPropagator(MdcKeyPolicy.allowAll()))
.propagator(OtelBaggagePropagator.create())
.build();
// Context entries automatically added to OTel BaggageList<String> results = ScopeFlowScoped.executeAll(scopeFlow, List.of(
() -> fetchFromServiceA(), // context propagated
() -> fetchFromServiceB(), // context propagated
() -> fetchFromServiceC() // context propagated
));// Block sensitive data from ever entering context
ScopeFlowBuilder.create()
.keyPolicy(ContextKeyPolicy.denyList(Set.of("password", "token")))
.build();With the starter, the following happens automatically:
| Feature | What it does |
|---|---|
| Request scope | Opens http.request scope per HTTP request with request.id, http.method, http.path |
| Request ID | Generates UUID or reads from X-Request-ID header |
| MDC | All context keys flow to SLF4J MDC for correlated logs |
| @Async | ScopeFlowTaskDecorator propagates context to async tasks |
| Virtual threads | Works with spring.threads.virtual.enabled=true |
# Filter which keys go to MDC (recommended for production)
scopeflow.mdc.keys=request.id,tenant.id,user.id
# Custom request ID header
scopeflow.http.server.request-id-header=X-Correlation-ID
# Disable features
scopeflow.mdc.enabled=false
scopeflow.http.server.generate-request-scope=false📖 Full documentation available in docs/wiki/:
- Getting Started
- Core Concepts
- Spring Boot Integration
- Context Propagation
- MDC Logging
- Micrometer Integration
- OpenTelemetry Integration
- ScopedValue & StructuredTaskScope
- Configuration Reference
- Best Practices
- Java 21+ (Java 23+ for
scopeflow-scopedmodule) - SLF4J 2.x (for MDC module)
- Spring Boot 3.2+ (for starter)
mvn clean verify
# 96 tests, 11 modules, ~8 seconds