# 2. @Async, DelegatingSecurityContextExecutor, and virtual threads on Boot 4.1 [← Prev: InheritableThreadLocal](01-inheritable-threadlocal.md) | [Next: Structured concurrency →](03-structured-concurrency.md) `Demo2AsyncVirtualThreads.java` reproduces the exact executor bean Spring Boot 4.1 wires up when you set `spring.threads.virtual.enabled=true`: a `SimpleAsyncTaskExecutor` with `setVirtualThreads(true)`. That bean backs `@Async`, MVC async request handling, and WebFlux's blocking-execution support. It is not a `ThreadPoolTaskExecutor` and never has a fixed pool of workers to reuse -- see [Chapter 1](01-inheritable-threadlocal.md) for why that matters. Four scenarios, same question each time: does the async task see the `Authentication` that was active on the calling thread? Full output in [`docs/output/demo2.txt`](output/demo2.txt). ## A) Default mode, unwrapped executor -- loses it `SecurityContextHolder`'s default strategy, `MODE_THREADLOCAL`, does not travel to any new thread, virtual or not. This is the exact symptom reported against Spring Security as [gh-15040](https://github.com/spring-projects/spring-security/issues/15040): swap in a raw virtual-thread executor and `@Async` methods start throwing `AccessDeniedException` because `SecurityContextHolder.getContext().getAuthentication()` is `null`. ## B) MODE_INHERITABLETHREADLOCAL, unwrapped executor -- works This is the finding from Chapter 1 applied to Spring Security directly. Because the virtual thread the executor spins up is fresh every time, `MODE_INHERITABLETHREADLOCAL` propagates the context correctly with **zero extra wrapping code**. The reference docs' warning against this mode predates virtual threads and is about pooled platform threads specifically -- it does not apply to this executor shape. This is still a global JVM-wide setting, so weigh that against the next two options, which are scoped to one executor bean. ## C) DelegatingSecurityContextExecutor -- still works, unconditionally `DelegatingSecurityContextExecutor` doesn't rely on thread-local inheritance at all -- it wraps the submitted `Runnable`, and the wrapper explicitly calls `SecurityContextHolder.setContext(...)` / `clearContext()` around the delegate's `run()`, wherever that `run()` happens to execute. That is why it has worked, unchanged, since long before virtual threads existed, and why it is still the correct choice for library code that cannot assume the application has set `MODE_INHERITABLETHREADLOCAL` globally. ## D) ContextPropagatingTaskDecorator -- the mechanism that's actually new This is the one that did not exist when the [original version of this post](https://ankurm.com/spring-security-context-propagation-complete-guide/) went up. Spring Security 6.5 (GA 2025-05-19) added `SecurityContextHolderThreadLocalAccessor`, which self-registers with Micrometer's `ContextRegistry` via `ServiceLoader` the moment `io.micrometer:context-propagation` is on the classpath -- no bean, no configuration. Spring Framework's `ContextPropagatingTaskDecorator` (since 6.1) uses that registry to snapshot and restore every registered `ThreadLocalAccessor` around a task. Set it as the executor's task decorator and `@Async` methods get the `SecurityContext` back **without any `DelegatingSecurityContext*` wrapper at all** -- and the same decorator simultaneously restores MDC and tracing context, which the `Delegating*` classes never touched. ```java SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(); executor.setVirtualThreads(true); executor.setTaskDecorator(new ContextPropagatingTaskDecorator()); ``` Spring Security's own [Concurrency Support page](https://docs.spring.io/spring-security/reference/features/integrations/concurrency.html) still only documents the `Delegating*` family as of 7.1.1 -- this pattern is real and shipped, just not yet reflected in that page. `io.micrometer:context-propagation` is already on the classpath of any Boot 4.1 app that pulls in `micrometer-observation` (actuator, tracing, or `spring-boot-starter-micrometer-*`). If your app doesn't have a Micrometer dependency anywhere, add `io.micrometer:context-propagation:1.2.1` (the version Boot 4.1.1's BOM manages) explicitly.