Summary
Proposing an optional, auto-configured MDC propagation for the auto-configured TaskExecutor (used by @Async).
Optionally, this could be extended to the auto-configured TaskScheduler.
Spring Boot would provide an MdcTaskDecorator that copies org.slf4j.MDC from the submitting thread to the worker thread, enabled via:
# using a property such as:
spring.task.execution.mdc-propagation=true
Motivation
Spring Boot already composes multiple TaskDecorator beans for the auto-configured executors, so an MDC-aware decorator would naturally fit into this mechanism.
MDC works well for synchronous request processing, but breaks in async flows:
- MDC is ThreadLocal-based
@Async,TaskExecutor,CompletableFuturerun on different threads- MDC values (e.g.
traceId) are not propagated
This is a common pain point. The typical workaround looks like:
public class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
Map<String, String> previous = MDC.getCopyOfContextMap();
try {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
else {
MDC.clear();
}
runnable.run();
}
finally {
if (previous != null) {
MDC.setContextMap(previous);
}
else {
MDC.clear();
}
}
};
}
}
Nearly every project re-implements this. It's easy to forget, and confusing when MDC "randomly disappears" only in async flows.
Proposed Enhancement
1. Configuration property
spring.task.execution.mdc-propagation=true
2. Conditional auto-configuration
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(org.slf4j.MDC.class)
@ConditionalOnProperty(
prefix = "spring.task.execution",
name = "mdc-propagation",
havingValue = "true"
)
class MdcTaskDecoratorConfiguration {
@Bean
@Order(Ordered.LOWEST_PRECEDENCE)
TaskDecorator mdcTaskDecorator() {
return new MdcTaskDecorator();
}
}
This integrates naturally with existing CompositeTaskDecorator composition—just one more decorator in the chain.
Alternatives Considered
- Status quo: Developers keep implementing their own. Works, but lots of duplicated boilerplate.
- Micrometer context-propagation: Covers tracing/Reactor contexts, but not simple MDC-only use cases. This proposal focuses on Boot-level UX: "enable async + MDC logging, it just works."
- Third-party starter: Possible, but MDC logging is common enough that built-in support seems reasonable.
Backwards Compatibility
- Opt-in only (property must be explicitly enabled)
- Only activates when SLF4J MDC is present
- Existing custom decorators continue to work via
@Orderor bean overrides
Open Questions
- Property naming:
spring.task.execution.mdc-propagationvs other alternatives? - Should this also apply to the auto-configured
TaskScheduler? - Any plans to back this with Micrometer context-propagation later?
I'd be happy to submit a PR if this direction makes sense.
Comment From: bclozel
Hello @devsigner9920 and thanks for your detailed proposal. Before analyzing in more details this issue, I'd like to understand a couple of things.
Any plans to back this with Micrometer context-propagation later?
I think this is already supported in micrometer context-propagation with the Slf4jThreadLocalAccessor. You can install it in any application with the following:
ContextRegistry.getInstance().registerThreadLocalAccessor(new Slf4jThreadLocalAccessor());
We do not use this accessor in Spring Boot nor provide an option for it because, as mentioned in its javadoc:
It is recommended to use only when no other
ThreadLocalAccessorinteracts with the MDC or the selected keys, for instance tracing libraries.
In Spring Boot, we already register the Observation thread local accessor that is in charge of propagating the current observation in thread locals and the MDC. Adding both accessors is likely to break the observability stack completely.
If the goal is to guarantee of trace ids in @Async methods, this is also already possible and documented, but we are also considering adding a new configuration property to make this easier (see #48033).
So, my questions are:
- are you only interested in trace ids or all MDC values?
- are you using micrometer or the opentelemetry SDK in your application?
- have you considered the solutions explained above?
Thanks!
Comment From: devsigner9920
Thanks for the detailed feedback and for pointing me to the existing context-propagation work.
After reviewing:
- Spring Framework's ContextPropagatingTaskDecorator
- Micrometer Context Propagation (ThreadLocalAccessor, ContextRegistry, ContextSnapshot)
- Related discussions (#39299, micrometer-metrics/context-propagation#191)
I now see that my proposal would introduce a parallel abstraction that overlaps with Micrometer's existing design. The proper layering should be:
- Micrometer: Core abstractions (
ThreadLocalAccessor,ContextRegistry) - Spring Framework: Integration point (
ContextPropagatingTaskDecorator) - Spring Boot: Auto-configuration and properties
To answer your questions directly:
-
Trace IDs vs all MDC values: I'm primarily interested in propagating business context (userId, correlationId, tenantId), not just trace IDs.
-
Micrometer vs OpenTelemetry SDK: We use Micrometer in most projects, but some simpler apps don't have any observability stack.
-
Existing solutions: Yes, I've reviewed them. The gap I see is discoverability – making async context propagation easy to enable with a simple property.
Given this, I'm happy to withdraw my original proposal in favor of #48033, which addresses exactly this gap.
I see that #48033 is already assigned to @sbera13. I don't want to step on anyone's toes, but if there's any way I can help – whether reviewing the PR, writing additional tests, or contributing to documentation – I'd be glad to assist.
Thanks again for the guidance!
Comment From: bclozel
Thanks for your feedback @devsigner9920
- Trace IDs vs all MDC values: I'm primarily interested in propagating business context (userId, correlationId, tenantId), not just trace IDs.
In this case, I guess you are creating your own ThreadLocalAccessor that saves/restores your custom thread locals for userId, tenantId, etc. This should be automatically installed if you declare it as a service.
- Micrometer vs OpenTelemetry SDK: We use Micrometer in most projects, but some simpler apps don't have any observability stack.
Thanks, noted. In this case the Slf4jThreadLocalAccessor is not really a good overall solution.
- Existing solutions: Yes, I've reviewed them. The gap I see is discoverability – making async context propagation easy to enable with a simple property.
This part should indeed be covered with #48033 - it's assigned right now but feel free to subscribe and we'll ask on the issue if the assigned contributor can't make it. I'm closing this issue in favor of the other one then.
Thanks!
Comment From: devsigner9920
@bclozel Thanks a lot for the detailed explanation and for closing the loop here. I appreciate the clarification and the pointer to #48033 – I’ll subscribe and follow along there.