-
Notifications
You must be signed in to change notification settings - Fork 0
Best Practices
This guide outlines architectural patterns, operational recommendations, and failure modes to consider when deploying ScopeFlow in high-concurrency production environments.
ScopeFlow scopes implement java.lang.AutoCloseable. They maintain an internal stack frame on the executing thread. If a scope is opened without structured closing, an uncaught exception will cause the stack frame to leak into subsequent requests executed on the same thread pool worker.
// Recommended: Deterministic cleanup guaranteed by language specification
try (Scope scope = scopeFlow.open("payment.authorization", contextMap)) {
executePayment();
}
// Anti-pattern: Unhandled exceptions bypass scope.close(), polluting the thread pool
Scope scope = scopeFlow.open("payment.authorization", contextMap);
executePayment(); // If this throws RuntimeException, scope leaks
scope.close();Nested scopes strictly follow Last-In, First-Out (LIFO) order. When closing nested scopes, the innermost scope must always close before the enclosing scope. Standard try-with-resources blocks enforce this automatically:
try (Scope outer = scopeFlow.open("http.request", Map.of("tenant.id", "acme"))) {
// Context contains: tenant.id=acme
try (Scope inner = scopeFlow.open("db.transaction", Map.of("tx.id", "tx-901"))) {
// Context contains: tenant.id=acme, tx.id=tx-901
executeDbCall();
} // inner closes: tx.id removed, tenant.id remains intact
} // outer closes: tenant.id removed, thread context restored to pre-request stateScopes should correspond to bounded operational units (e.g., an HTTP handler, a batch item processing loop, or a database transaction).
Avoid holding scopes open across indefinite I/O waits, WebSocket connections, or long-polling cycles. For long-running asynchronous tasks, capture a Snapshot or use scopeFlow.wrap() rather than keeping an ambient scope active on an idle thread.
A Scope instance is not thread-safe and must never be passed to or modified by concurrent threads. Attempting to call scope.close() or access its internal context from a different thread introduces race conditions and corrupts the thread's stack.
// Anti-pattern: Attempting to share a Scope across concurrent workers
try (Scope scope = scopeFlow.open("batch.process", metadata)) {
executor.submit(() -> {
// Race condition: concurrent thread accessing thread-local scope
doWork();
});
}
// Recommended: Capture an immutable snapshot via wrap()
try (Scope scope = scopeFlow.open("batch.process", metadata)) {
executor.submit(scopeFlow.wrap(() -> {
// Immutable context is safely cloned and bound to the worker thread
doWork();
}));
}In Spring Boot or containerized applications with centralized executors, decorate the Executor or ThreadPoolTaskExecutor at configuration time rather than instrumenting individual Runnable or Callable invocations:
@Configuration
public class AsyncConfiguration {
@Bean
public ThreadPoolTaskExecutor taskExecutor(ScopeFlow scopeFlow) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(16);
executor.setMaxPoolSize(64);
executor.setTaskDecorator(new ScopeFlowTaskDecorator(scopeFlow));
executor.initialize();
return executor;
}
}By default, any key added to ScopeContext is forwarded to active propagators (such as SLF4J MDC). In production, unmanaged propagation can accidentally expose Personally Identifiable Information (PII), authentication tokens, or credential headers in log aggregators (Elasticsearch, Datadog, Splunk).
Use MdcKeyPolicy.of() (allowlist) or ContextKeyPolicy.denyList() to enforce boundary constraints:
// Recommended: Explicit allowlist for production environments
@Bean
public MdcPropagator mdcPropagator() {
return new MdcPropagator(MdcKeyPolicy.of(Set.of(
"request.id",
"trace.id",
"tenant.id",
"user.role"
)));
}
// Security safeguard: Deny list blocking sensitive attributes
@Bean
public ScopeFlow scopeFlow() {
return ScopeFlowBuilder.create()
.keyPolicy(ContextKeyPolicy.denyList(Set.of(
"authorization",
"password",
"token",
"secret",
"cvv",
"ssn"
)))
.build();
}ScopeContext is designed to propagate operational identifiers and routing tags, not domain entity payloads:
- Good candidates for ScopeContext: Correlation IDs, user IDs, tenant IDs, transaction IDs, client IP addresses.
- Bad candidates for ScopeContext: JSON request bodies, large entity graphs, domain collections, binary buffers.
Storing large objects in ScopeContext leads to excessive heap retention and increases snapshot copying overhead across thread boundaries.
When used correctly with try-with-resources:
- Synchronous open/close overhead: ~15-25 nanoseconds on modern hardware.
- Allocation overhead: Zero heap allocations in steady-state when child scopes reuse immutable parent contexts.
-
Logback MDC synchronization: Bounded by SLF4J's internal
MDCAdapterimplementation.
| Symptom | Probable Cause | Corrective Action |
|---|---|---|
MDC logs show stale correlation IDs from previous requests |
Scope was opened without try-with-resources or was not closed during an exception. |
Audit codebase to ensure all scopeFlow.open() calls use try (Scope s = ...) syntax. |
Context not available in @Async Spring methods |
The task executor used by Spring does not have a ScopeFlowTaskDecorator. |
Register ScopeFlowTaskDecorator in the application's AsyncConfigurer or ThreadPoolTaskExecutor bean. |
| Memory usage grows linearly with virtual threads | Large objects stored in ScopeContext while spawning millions of virtual threads. |
Restrict context attributes strictly to lightweight string identifiers. |
| OpenTelemetry Baggage not appearing downstream | Downstream HTTP client headers do not include W3C baggage headers. | Ensure OpenTelemetry SDK W3CBaggagePropagator is registered in the tracer provider. |
ScopeFlow • Distributed Context & Tracing Propagation • Licensed under Apache-2.0