Skip to content

Best Practices

Frody edited this page Sep 3, 2026 · 2 revisions

Production Guidelines & Architecture Patterns

This guide outlines architectural patterns, operational recommendations, and failure modes to consider when deploying ScopeFlow in high-concurrency production environments.


1. Scope Lifecycle Management

Enforce Deterministic Unwinding via try-with-resources

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();

Nesting and LIFO Ordering

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 state

Scope Granularity & Lifetimes

Scopes 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.


2. Thread Safety and Concurrency

Scopes are Strictly Thread-Confined

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();
    }));
}

Prefer Executor Decoration over Manual Task Wrapping

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;
    }
}

3. Data Sanitization & Security

Enforce Explicit Key Policies (GDPR / PCI-DSS)

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();
}

4. Performance & Memory Considerations

Keep Context Payloads Bounded

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.

Benchmark Profile

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 MDCAdapter implementation.

5. Diagnostic Matrix

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.

Clone this wiki locally