Mastering MDC in SpringBoot: Propagate traceId Across Threads and HTTP Calls

This article explains how to use Mapped Diagnostic Context (MDC) in SpringBoot to store and propagate traceId across threads and HTTP requests, covering MDC basics, API, advantages, common pitfalls, and practical solutions with interceptors, thread‑pool wrappers, and log pattern configuration.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Mastering MDC in SpringBoot: Propagate traceId Across Threads and HTTP Calls

MDC (Mapped Diagnostic Context) is a feature provided by log4j, logback and log4j2 that allows storing key‑value pairs in a hash table bound to the current thread, making the data accessible to any code executed in that thread.

When a child thread is created it inherits the parent’s MDC, and the typical usage in a web application is to store request‑related data (e.g., traceId) at the beginning of request processing.

API

clear(): remove all entries

get(String key): get value for key

getContext(): get the whole MDC map

put(String key, Object o): add entry

remove(String key): delete entry

Advantages

Code becomes concise and log format is unified; you can log traceId without manually concatenating it, 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 {
        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) {}
    @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        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 point is to include %X{traceId} in the pattern, matching the MDC key name.

Problems with MDC

traceId lost in child threads

traceId lost in HTTP calls

Solutions

Child‑thread traceId loss

Wrap tasks submitted to a thread pool so that the MDC map is copied to the worker thread.

public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
    // constructors omitted for brevity
    @Override
    public void execute(Runnable task) {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
    @Override
    public <T> Future<T> submit(Runnable task, T result) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
    }
    @Override
    public <T> Future<T> submit(Callable<T> 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 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 <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> 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<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

HTTP‑call traceId loss

For each HTTP client library, add an interceptor that reads the traceId from MDC and injects it into the request header.

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

Configure the client:

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

Configure the client:

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

Register the interceptor:

restTemplate.setInterceptors(Collections.singletonList(new RestTemplateTraceIdInterceptor()));

Third‑party service interceptor

On the receiving side, add a handler interceptor that extracts the traceId from the incoming request header and puts it into MDC, generating 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("traceId", traceId);
        return true;
    }
    @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {}
    @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        MDC.remove(Constants.TRACE_ID);
    }
}

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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpringBoottraceidmdcthread-pool
Java High-Performance Architecture
Written by

Java High-Performance Architecture

Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.

0 followers
Reader feedback

How this landed with the community

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.