Backend Development 11 min read

Using MDC for TraceId Propagation in Java Backend Applications

This article explains what MDC (Mapped Diagnostic Context) is, its API and advantages, demonstrates how to integrate it into Spring MVC interceptors and log patterns, and provides solutions for traceId loss in child threads and HTTP calls using custom thread‑pool wrappers and HTTP client interceptors.

Architecture Digest
Architecture Digest
Architecture Digest
Using MDC for TraceId Propagation in Java Backend Applications

Through this article you will learn what MDC (Mapped Diagnostic Context) is, the problems that can appear when using MDC in a web application, and how to solve those problems.

MDC Introduction

MDC is a feature provided by log4j, logback and log4j2 that allows a hash table bound to the current thread to store key‑value pairs, which can be accessed by any code running in the same thread. Child threads inherit the parent thread’s MDC content, making it convenient for logging request‑related data such as a traceId.

MDC API

clear() – remove all entries

get(String key) – retrieve the value for a key

getContext() – obtain the whole MDC map

put(String key, Object o) – add a key‑value pair

remove(String key) – delete a specific entry

Advantages

Code stays concise and log format is unified; no need to manually concatenate traceId in each log statement.

MDC Usage – Adding an Interceptor

public class LogInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // Use upstream traceId if present
        String traceId = request.getHeader(Constants.TRACE_ID);
        if (traceId == null) {
            traceId = TraceIdUtil.getTraceId();
        }
        MDC.put(Constants.TRACE_ID, traceId);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        MDC.remove(Constants.TRACE_ID);
    }
}

Log pattern must include the MDC key:

<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>

Problems with MDC

TraceId is lost in child threads.

TraceId is lost during HTTP calls.

Solution for Child‑Thread TraceId Loss – ThreadPool Wrapper

public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue
workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }
    // other constructors omitted for brevity
    @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()));
    }
}

Utility class that wraps tasks with the current MDC context:

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

Implement interceptors for the most common HTTP clients and add the traceId header.

HttpClient interceptor :

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);
        }
    }
}

Register it:

private static CloseableHttpClient httpClient = HttpClientBuilder.create()
        .addInterceptorFirst(new HttpClientTraceIdInterceptor())
        .build();

OkHttp interceptor :

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);
    }
}

Register it:

private static OkHttpClient client = new OkHttpClient.Builder()
        .addNetworkInterceptor(new OkHttpTraceIdInterceptor())
        .build();

RestTemplate interceptor :

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);
    }
}

Register it:

restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));

For third‑party services, a similar interceptor must be added on the callee side to read the header and put the value into MDC.

Finally, ensure the logging pattern contains %X{traceId} so that the traceId appears in every log line.

backendJavathreadpoolloggingTraceIdHTTP InterceptorMDC
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.