# 4. Executor, ExecutorService, and AsyncTaskExecutor wrapping [← Prev: Structured concurrency](03-structured-concurrency.md) | [Next: Reactive context →](05-reactive-context.md) Chapters 1–3 are all about virtual threads and structured concurrency, which came later. `Demo4ExecutorWrapping.java` goes back to the baseline the post's "Using @Async", "Using ExecutorService", and "Using CompletableFuture" sections describe: a **fixed platform-thread pool** (`ThreadPoolExecutor`, `ThreadPoolTaskExecutor`), the shape almost every Spring app used before `spring.threads.virtual.enabled` existed, and the exact pooled-thread danger [Chapter 1](01-inheritable-threadlocal.md) demonstrated for `InheritableThreadLocal`. Full output in [`docs/output/demo4.txt`](output/demo4.txt). ## The three wrapper classes the post names - **`DelegatingSecurityContextExecutorService`** wraps an entire `ExecutorService` -- `execute()`, `submit()`, `invokeAll()`, `invokeAny()` all go through the wrapper. This is the fix for the post's `TaskExecutionService` ("Using ExecutorService") example. - **`DelegatingSecurityContextExecutor`** wraps a plain `Executor` and is what you hand to `CompletableFuture.supplyAsync(supplier, executor)` -- the fix for `CompletableFutureService` ("Using CompletableFuture"). The common `ForkJoinPool` that `CompletableFuture.supplyAsync(supplier)` uses when you don't supply an executor never propagates context; scenario C2 in the output shows that directly. - **`DelegatingSecurityContextAsyncTaskExecutor`** wraps Spring's own `AsyncTaskExecutor`/`TaskExecutor` abstraction -- the type `AsyncConfigurer#getAsyncExecutor()` actually returns, and the object Spring's `@Async` infrastructure calls `execute()`/`submit()` on. This is the fix for the post's `AsyncConfig` example. All three extend the same mechanism [Chapter 2](02-async-virtual-threads.md) already described: wrap the submitted `Runnable`/`Callable`, capture `SecurityContextHolder.getContext()` once (at wrap time, not at thread-construction time), and push/pop it around the delegate's execution on whatever thread that turns out to be. ## The edge case Chapter 1 sets up and this chapter resolves Chapter 1's whole point was that a **reused pool worker** keeps whatever `InheritableThreadLocal` value existed when the pool created it, not what the submitting thread had at submission time -- that's why `MODE_INHERITABLETHREADLOCAL` is dangerous with a fixed thread pool. The `EDGE` scenario in this demo asks the same question of the `Delegating*` wrappers, and the answer is the opposite: ``` EDGE) task 1 on possibly-reused worker: authenticated as carol-task1 EDGE) task 2, same pool, different caller context: authenticated as dave-task2 <-- correct, NOT stale ``` Two tasks submitted through `DelegatingSecurityContextExecutorService` to the *same* two-worker pool, with the caller's `SecurityContextHolder` context changed in between, each see their **own** context -- never the other task's. That's because the wrapper captures context per submission (inside `wrap()`, called synchronously from `execute()`/`submit()`), not once per worker thread the way thread-local inheritance does. This is the actual reason the `Delegating*` family predates virtual threads by years and is still correct on a fixed pool: it never depended on thread identity in the first place. [Chapter 8](08-testing-contract.md) pins this exact contrast as a JUnit assertion (`delegatingSecurityContextExecutorServicePropagatesAndCapturesIndependentlyPerTask`), not just a printed line. ## Practical note: which one do you actually need? If you already have an `AsyncConfigurer` returning a `ThreadPoolTaskExecutor`, wrap it with `DelegatingSecurityContextAsyncTaskExecutor` and nothing else changes -- `@Async` keeps working as written. If you're calling `CompletableFuture.supplyAsync(...)` without an explicit executor anywhere in the codebase, that's the common-`ForkJoinPool` trap in scenario C2; the fix is always to supply a `DelegatingSecurityContextExecutor`-wrapped executor, never to reach for `MODE_INHERITABLETHREADLOCAL` as a global patch for one call site.