Backend Development 12 min read

MDC (Mapped Diagnostic Context) in Java: Introduction, API, Advantages, and Practical Solutions for TraceId Propagation

This article introduces MDC, explains its API and benefits, and provides concrete solutions—including a thread‑pool wrapper and HTTP client interceptors—to reliably propagate traceId across threads and remote calls in Java backend applications.

Top Architect
Top Architect
Top Architect
MDC (Mapped Diagnostic Context) in Java: Introduction, API, Advantages, and Practical Solutions for TraceId Propagation

This article gives a comprehensive overview of MDC (Mapped Diagnostic Context), a feature provided by log4j, logback, and log4j2 that allows storing key‑value pairs bound to the current thread for easier logging in multi‑threaded environments.

1. Introduction

MDC works like a thread‑local hash map; child threads inherit the parent’s MDC content. In a web request, MDC is typically populated at the beginning of request processing.

2. API

clear(): remove all entries

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

getContext(): obtain the current MDC map

put(String key, Object value): store a key‑value pair

remove(String key): delete a specific entry

3. Advantages

Using MDC makes log statements concise and uniform, e.g., LOGGER.info("traceId:{} ", traceId) , without manually concatenating the traceId in each log.

4. Practical Usage

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

4.2 Modify log pattern

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

5. Problems with MDC

TraceId lost in child threads

TraceId lost in HTTP calls

These issues are solved step by step without premature optimization.

6. Solution for child‑thread loss

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

7. MDC utility for wrapping tasks

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

8. HTTP‑call traceId propagation

For HttpClient, OkHttp, and RestTemplate, custom interceptors add the traceId header and the downstream service can read it back into MDC.

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

Adding the interceptor:

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

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

Adding the interceptor:

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

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

Adding the interceptor:

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

All three solutions ensure that the traceId is carried through asynchronous tasks and remote HTTP calls, allowing end‑to‑end request tracing in backend services.

Finally, the article reminds readers to include %X{traceId} in the log pattern to actually print the traceId.

backendJavathreadpoolloggingInterceptorTraceIdMDC
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

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.