skip to content

SLF4J 2.0 added a fluent logging API entered through methods such as `atInfo()` and `atDebug()`. What does it offer over the classic `info(String, Object...)` calls, and how do its key-value pairs differ from Mapped Diagnostic Context entries and from Markers?

level: middleimportance: nice to knowfreq 25%

answer

  1. atInfo() builder, terminate with log()
  2. disabled level returns a no-op builder
  3. supplier arguments defer expensive computation
  4. kv = per event, MDC = per thread
  5. marker = filter/route tag, carries no value

basics

~20 s

The fluent API builds an event step by step: message, arguments, key-value pairs, a marker, a cause, then log. Its key-value pairs are per-event structured fields; MDC entries are per-thread ambient context; markers are labels for routing and filtering, not data.

solid answer

~60 s

`atInfo()` returns a builder on which you set a message with placeholders, add arguments, attach typed key-value pairs, add a marker, set the throwable, and finish with `log()`. If the level is disabled the builder is a no-op instance, so nothing is assembled. Three benefits: it reads better when an event has many attachments; it can take supplier-style arguments so an expensive value is only computed when the event will actually be emitted; and it makes structured fields first-class rather than smuggled into the message text. The distinctions matter. A **key-value pair** belongs to one event and is supplied at the call site — the natural home for the order id or the duration of this operation. **MDC** is ambient per-thread context set at a boundary and stamped on every line until cleared — the natural home for request or tenant identity. A **Marker** is a named tag with no value, used to route or filter (for example an audit or security marker) rather than to carry data. Whether a backend renders key-value pairs depends on its encoder.

code

text · 6 lines
text
classic : log.info("order {} shipped in {} ms", id, ms)
fluent  : atInfo().setMessage("order shipped")
                  .addKeyValue("orderId", id)
                  .addKeyValue("durationMs", ms)
                  .log()
ambient : MDC holds requestId/tenant for every line of the request

go deeper

for a junior

Know that a builder-style alternative exists in SLF4J 2.0 and that key-value pairs attach named data to a single event.

for a middle

Contrast the three mechanisms cleanly — per-event fields, per-thread context, filter tags — and mention the no-op builder and supplier arguments.

for a senior

Discuss when structured fields are worth the churn, how rendering depends on the encoder, and how to keep the field vocabulary small enough to stay queryable.

for a principal

Decide the logging contract for a platform: which fields are ambient, which are per-event, which markers exist and what they route to, and how the schema is governed as services multiply.

## What the fluent API is SLF4J 2.0 introduced a second way to log alongside the classic methods. Instead of one call that must express everything through its parameter list, you enter a builder with a level-specific method (`atTrace`, `atDebug`, `atInfo`, `atWarn`, `atError`, or a level-parameterised `atLevel`), attach the parts of the event, and terminate with `log()`. Nothing is emitted until the terminating call. The classic API is not deprecated and is still the right choice for ordinary lines. The fluent API exists because the classic parameter list had run out of room: a message, N arguments, an optional trailing throwable, and an optional leading marker is already a crowded signature, and there was nowhere to put structured fields at all. ## The three benefits **Cheap disabled path.** When the level is disabled, the entry method returns a shared no-op builder, so every subsequent call does nothing and no event object is built. This preserves the guarantee people rely on from placeholder logging. **Deferred argument computation.** The builder accepts supplier-style arguments, which are only invoked when the event is actually emitted. This closes the gap left by the classic API, where an expensive argument expression is evaluated eagerly even though the formatting is deferred — the case that used to require an explicit `isDebugEnabled()` guard. **Structured fields as data, not prose.** Key-value pairs attach typed values to the event under a name, instead of being flattened into the sentence. A backend with a structured encoder can emit them as real fields, so a query can filter on the value rather than on a substring of the message. The message can stay a stable, low-cardinality description. ## Key-value pairs versus MDC Both end up as fields in structured output, which is why they get confused. The difference is scope and lifecycle. - A key-value pair is **per event, supplied at the call site**, and disappears afterwards. Use it for facts about this specific occurrence: the identifier that was not found, the elapsed milliseconds, the retry attempt number. - MDC is **per thread, ambient**, set once at a boundary and printed on every event until it is cleared. Use it for identity that is true of the whole unit of work: request id, trace id, tenant, principal. Using MDC for per-event data means putting and removing entries around individual log calls, which is noisy and leaks when an exception escapes. Using key-value pairs for request identity means repeating the same pair on every call site, which is exactly what MDC was invented to avoid. The rule of thumb: if a value is true for the whole request, it is context; if it is true for this one line, it is a field. ## Markers A `Marker` is a named tag, optionally with child markers, attached to an event. It carries no value and is not data. Its purpose is *routing and filtering* in the backend: an audit marker sent to a separate destination, a marker that forces an event through despite the level, a marker that suppresses an event from the main stream. Because markers are matched by the backend's filters, they only do something if the backend is configured for them; an unconfigured marker is invisible. Do not use markers to smuggle values by encoding them into the marker name — that creates unbounded marker cardinality and defeats filtering. ## Practical caveats Rendering is backend-dependent. A plain pattern layout may ignore key-value pairs entirely unless the pattern references them, so a team can add pairs and see nothing; verify against the actual encoder. Adoption is also gradual — a codebase will mix both APIs for a long time, and that is fine, since both produce the same kind of event. Finally, discipline about key names matters more than the mechanism: field names should be a small, agreed vocabulary, otherwise a structured store fills with near-synonyms and nothing is queryable.

  • Does the fluent API remove the need for an isDebugEnabled() guard around an expensive argument?
    Yes, when you pass the argument as a supplier rather than as a value, because the supplier is only invoked if the event is actually emitted. If you pass an already-computed value, the cost is paid at the call site exactly as with the classic API, and the guard still has a purpose. The entry method itself is already cheap on a disabled level, since it returns a shared no-op builder.
  • When would you still prefer the classic info(String, Object...) call?
    For ordinary lines with a message and one or two parameters, where the classic form is shorter and just as capable, and in code that must compile against SLF4J 1.7.x. The fluent API earns its verbosity when an event has several structured fields, a marker, a cause, and deferred arguments — that is, when the classic parameter list would be doing too many jobs at once.

saying these in an interview costs you the question

  • Treating key-value pairs and MDC as interchangeable, so request identity is repeated on every call or per-event data is pushed through the thread context.
  • Encoding values into marker names, which makes marker cardinality unbounded and filtering useless.
  • Assuming key-value pairs appear in the output regardless of the backend's encoder configuration.
  • Believing the fluent API replaces the classic methods or that the classic ones are deprecated.
  • Thinking the builder allocates work even when the level is disabled.

context