A customer reports that a payment failed at "around 3:40 pm." The request passed through an API gateway, three Spring Boot microservices and a Kafka topic. Each service logs hundreds of lines a second. Without a shared identifier, finding that one request means grepping by timestamp and hoping.
On the development side, I used to think of logging as something for me. On the support side, I've learned the truth: logs are written for a stranger at 2am — someone who has never seen your code and needs to trace one failed request in minutes. The single most useful thing you can give that stranger is a correlation ID.
First: do you already have one?
If your services use Micrometer Tracing (with the OpenTelemetry or Brave bridge), Spring Boot already puts a traceId and spanId into the logging context and propagates them across HTTP calls. In recent Spring Boot 3 versions these IDs appear in log lines automatically. If that's you, use the trace ID as your correlation ID and skip to the section on async code.
If you don't run tracing — common in smaller teams and monoliths — a small filter gets you most of the value.
Step 1: tag every request with a filter
The filter reuses an incoming ID if the caller sent one (so the ID survives across services), generates one if not, stores it in SLF4J's MDC (Mapped Diagnostic Context), and echoes it back in the response header.
Two details matter here. The regex check stops a caller from injecting newlines or junk into your logs through the header. And the finally block matters because servlet threads are reused: forget it, and the next request on that thread inherits a stale ID.
Step 2: print it on every log line
Add the MDC value to your log pattern. %X{correlationId:-} prints the ID, or nothing if there isn't one.
Better still, log in JSON. From Spring Boot 3.4, structured logging is built in — one property, and every log line becomes a JSON object that includes your MDC values as fields, ready for Elasticsearch, Loki, Splunk or CloudWatch to filter on.
Step 3: pass it to the next service
A correlation ID that stops at the service boundary is only half useful. Add it to outgoing HTTP calls so downstream services pick it up in their own filter:
For Kafka, the same idea applies: put the ID in a record header when producing, and read it back into the MDC at the start of your listener.
Step 4: don't lose it in async code
MDC is backed by a ThreadLocal. The moment work moves to another thread — @Async, a CompletableFuture, a thread pool — the ID silently disappears, and those are often the exact log lines you need. A TaskDecorator copies the context across:
Spring Boot applies a TaskDecorator bean to its auto-configured task executor automatically. If you build your own executors, call setTaskDecorator(...) on them yourself.
Step 5: close the loop with the user
Return the ID in every error response and show it in the UI as a short reference. When a user says "it failed, reference 7f3c9a1e," support goes straight to the right log lines. I show how to include it in API errors in Spring Boot error handling done right.
finally block.