Using MDC for TraceId Propagation in Java Backend Applications
This article explains what MDC (Mapped Diagnostic Context) is, outlines its API, demonstrates how to configure logging and interceptors, and provides concrete solutions—including a custom thread‑pool wrapper and HTTP client interceptors—to ensure TraceId is correctly propagated across threads and remote calls in Java backend systems.
MDC (Mapped Diagnostic Context) is a feature provided by logging frameworks such as log4j, logback, and log4j2 that allows a hash map bound to the current thread to store key‑value pairs, making contextual information like a TraceId available to all log statements executed in that thread.
API
clear() – removes all entries from MDC
get(String key) – retrieves the value for a given key
getContext() – obtains the entire MDC map
put(String key, Object value) – stores a key‑value pair
remove(String key) – deletes a specific entry
Typical usage involves adding an interceptor that extracts a TraceId from the incoming request and puts it into MDC at the beginning of request processing.
LOGGER.info("traceId:{} ", traceId)However, TraceId can be lost in two common scenarios: child threads and HTTP calls to downstream services.
Solution for Child‑Thread TraceId Loss
Wrap the thread pool so that the MDC context is transferred to worker threads. The custom ThreadPoolExecutorMdcWrapper class overrides execution methods and uses ThreadMdcUtil.wrap to propagate the context.
public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue
workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable task) {
super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public
Future
submit(Runnable task, T result) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
}
@Override
public
Future
submit(Callable
task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public Future
submit(Runnable task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
}The utility class ThreadMdcUtil handles the actual wrapping of Runnable and Callable objects, setting the MDC map before execution and clearing it afterwards.
public class ThreadMdcUtil {
public static void setTraceIdIfAbsent() {
if (MDC.get(Constants.TRACE_ID) == null) {
MDC.put(Constants.TRACE_ID, TraceIdUtil.getTraceId());
}
}
public static
Callable
wrap(final Callable
callable, final Map
context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
return callable.call();
} finally {
MDC.clear();
}
};
}
public static Runnable wrap(final Runnable runnable, final Map
context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}Solution for HTTP Call TraceId Loss
For outbound HTTP calls, add an interceptor that copies the TraceId from MDC into the request header. Below are examples for three common HTTP clients.
HttpClient
public class HttpClientTraceIdInterceptor implements HttpRequestInterceptor {
@Override
public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
String traceId = MDC.get(Constants.TRACE_ID);
if (traceId != null) {
httpRequest.addHeader(Constants.TRACE_ID, traceId);
}
}
}
private static CloseableHttpClient httpClient = HttpClientBuilder.create()
.addInterceptorFirst(new HttpClientTraceIdInterceptor())
.build();OkHttp
public class OkHttpTraceIdInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
String traceId = MDC.get(Constants.TRACE_ID);
Request request = chain.request();
if (traceId != null) {
request = request.newBuilder().addHeader(Constants.TRACE_ID, traceId).build();
}
return chain.proceed(request);
}
}
private static OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new OkHttpTraceIdInterceptor())
.build();RestTemplate
public class RestTemplateTraceIdInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] body, ClientHttpRequestExecution execution) throws IOException {
String traceId = MDC.get(Constants.TRACE_ID);
if (traceId != null) {
httpRequest.getHeaders().add(Constants.TRACE_ID, traceId);
}
return execution.execute(httpRequest, body);
}
}
restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));For services that receive the request, a server‑side interceptor extracts the TraceId from the header and puts it back into MDC, creating a new one if absent.
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String traceId = request.getHeader(Constants.TRACE_ID);
if (traceId == null) {
traceId = TraceIdUtils.getTraceId();
}
MDC.put(Constants.TRACE_ID, traceId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
MDC.remove(Constants.TRACE_ID);
}
}Finally, modify the logging pattern to include the TraceId:
<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>With these configurations, TraceId is consistently propagated across threads and HTTP calls, enabling reliable end‑to‑end tracing in Java backend services.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.