Interview Prep Zoneby CuriouserLabs

Question 4 of 5 · staff level

How does distributed tracing work in Spring Boot 3 — context propagation, sampling, and what you actually use it for?

🎤 Say this first

Micrometer Tracing (Sleuth's successor) + an OTel/Brave bridge: each request gets a trace id; each unit of work a span; context propagates in-process (ThreadLocal/context snapshots) and cross-service via W3C traceparent headers, auto-instrumented for WebMVC/WebClient/RestTemplate/Kafka/etc. Export via OTLP to Tempo/Jaeger/vendor. Sampling is the architectural knob: head-based % for cost control, tail-based (collector-side) to keep the errors and slow traces you actually investigate. Value: pinpointing where latency lives in a fan-out, and correlating logs↔traces via trace ids in MDC.

The full picture

  • Mechanics to name: traceparent: 00-<traceId>-<spanId>-<flags> header; Observation API creates spans + metrics from one instrumentation; MDC gets traceId/spanId so every log line joins the trace (logging.pattern.correlation out of the box in Boot 3.2+).
  • The propagation gotchas: thread hops (@Async, executors) need context-propagation wrappers (ContextExecutorService, Reactor hooks); Kafka consumers start new traces linked via headers; virtual threads work but ThreadLocal-heavy custom propagation deserves an audit.
  • Sampling strategy: 100% in staging; production head-sampling (1–10%) + always-sample-on-error/slow via tail sampling in the OTel Collector — the traces you keep should be the ones you'd grep for.
  • What tracing answers that metrics can't: 'which downstream of the 7 in this request was slow, for THIS request' — metrics aggregate, traces narrate. Logs answer 'what did the code decide'. Three signals, three questions.
  • Baggage (propagated key-values, e.g., tenantId) is powerful and dangerous — every hop carries it; keep the allowlist tiny.
Custom span around business work
private final ObservationRegistry observations;

Order process(Command cmd) {
    return Observation.createNotStarted("order.process", observations)
        .lowCardinalityKeyValue("type", cmd.type().name())   // metric tag + span tag
        .highCardinalityKeyValue("orderId", cmd.orderId())   // span-only (cost-aware!)
        .observe(() -> doProcess(cmd));                      // span + timer, one API
}

🔄 Likely follow-up questions

  • Head vs tail sampling — infrastructure implications of each?
  • How do you trace through Kafka (span links vs parent-child) and why does it matter?
  • What breaks in trace context with @Async and how do you fix it?