# 3. StructuredTaskScope and SecurityContext [← Prev: Async + virtual threads](02-async-virtual-threads.md) | [Next: Executor/ExecutorService wrapping →](04-executor-wrapping.md) `Demo3StructuredConcurrency.java` asks the Chapter 2 question again, but for `StructuredTaskScope` (JEP 505, fifth preview in JDK 25 -- still preview through the JDK 26 sixth preview per JEP 525, so every example here needs `--enable-preview`). A `fork()` call starts a brand new virtual thread for the subtask, same as the executors in Chapter 2, so the Chapter 1 finding applies here too. Full output in [`docs/output/demo3.txt`](output/demo3.txt). ## What the JEP actually promises JEP 525's text is explicit about one kind of context and silent about another: > Subtasks forked in a scope inherit `ScopedValue` bindings. That is a real, specified guarantee -- and it says nothing about `ThreadLocal`. Spring Security's `SecurityContextHolder` is a `ThreadLocal`/`InheritableThreadLocal`, not a `ScopedValue`. Nothing in the structured concurrency API changes that, and scenario A below proves it: a plain `scope.fork(...)` with the default `MODE_THREADLOCAL` strategy loses the `Authentication` exactly like the unwrapped executor in Chapter 2 did. ## Four scenarios - **A) Plain fork, MODE_THREADLOCAL** -- lost. The default `SecurityContextHolder` strategy isn't inherited by anything, structured concurrency included. - **B) Plain fork, MODE_INHERITABLETHREADLOCAL** -- propagates. Same reasoning as Chapter 2, scenario B: `fork()`'s subtask thread is a fresh virtual thread, so inheritance at construction time works and there is no pooled-thread staleness risk. - **C) Manual capture-and-restore around the forked `Callable`** -- propagates, and does not depend on the global strategy mode at all: ```java SecurityContext captured = SecurityContextHolder.getContext(); Callable task = () -> { SecurityContextHolder.setContext(captured); try { return doWork(); } finally { SecurityContextHolder.clearContext(); } }; scope.fork(task); ``` This is the safest pattern for a `StructuredTaskScope` used inside library code, the same way `DelegatingSecurityContextExecutor` is the safest pattern for an `Executor`: it works regardless of what the surrounding application has set `SecurityContextHolder`'s strategy to. - **D) `ContextSnapshot.wrap(...)` around the forked `Callable`** -- the Chapter 2 mechanism applied to `fork()` instead of `execute()`. Because `SecurityContextHolderThreadLocalAccessor` is already registered with Micrometer's `ContextRegistry`, `ContextSnapshotFactory.builder() .build().captureAll()` picks up the current `SecurityContext` (and MDC, and tracing context) in one call, and `.wrap(callable)` restores all of them inside the subtask. This is the version worth reaching for once you have more than the `SecurityContext` to carry across the scope boundary. ## The practical takeaway `StructuredTaskScope` does not give `SecurityContextHolder` anything for free. If your `fork()`ed subtasks need to call secured services, wrap them explicitly -- option C if you want zero new dependencies, option D if `context-propagation` is already on the classpath and you have other thread-locals to carry along too.