Backend Development 12 min read

Using MDC for TraceId Propagation in Java Backend Applications

This article explains how to use Mapped Diagnostic Context (MDC) in Java logging frameworks to propagate traceId across threads and HTTP calls, covering API usage, advantages, common pitfalls, and practical solutions including interceptor implementation, thread‑pool wrappers, and client‑side interceptors for HttpClient, OkHttp, and RestTemplate.

Top Architect
Top Architect
Top Architect
Using MDC for TraceId Propagation in Java Backend Applications

Hello everyone, I am a senior architect.

1. Introduction: MDC (Mapped Diagnostic Context) is a feature provided by log4j, logback, and log4j2 that allows storing key‑value pairs bound to the current thread, making them accessible to any code executed in the same thread. Child threads inherit the MDC of their parent, which is useful for logging request‑specific data such as traceId.

2. API Description:

clear(): remove all entries from MDC

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

getContext(): obtain the entire MDC map

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

remove(String key): delete a specific entry

3. Advantages: Simplifies code, unifies log format, and eliminates manual string concatenation for traceId, e.g., LOGGER.info("traceId:{} ", traceId) .

MDC Usage

1. Add Interceptor

public class LogInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // If there is an upstream call, use its ID
        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 {
        // Remove after request finishes
        MDC.remove(Constants.TRACE_ID);
    }
}

2. Modify Log Pattern

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

The key part is %X{traceId} , which must match the MDC key name.

Problems with MDC

TraceId lost in child thread logs

TraceId lost in HTTP calls

These issues are solved incrementally.

Child Thread Log TraceId Loss

Solution: wrap thread‑pool tasks to copy MDC context.

public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
    // 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()));
    }
}

ThreadMdcUtil

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

Explanation: The wrapper copies the parent thread’s MDC map to the child thread, ensures traceId exists, runs the task, and finally clears MDC.

HTTP Call TraceId Loss

When making HTTP calls to third‑party services, the traceId must be added to request headers and extracted on the receiving side.

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

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

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

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

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

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

4. Third‑Party Service Interceptor

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("traceId", traceId);
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        MDC.remove(Constants.TRACE_ID);
    }
}

All interceptors ensure the traceId is propagated and logged consistently. Finally, the log pattern must include %X{traceId} to output the traceId.

Note: The %X{traceId} placeholder is required in the log pattern.

At the end, the author offers a BAT interview question collection and a QR code for a special gift, encouraging readers to join the architecture community.

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.