Why this matters: logging frameworks accumulate requirements for a decade — every one of the variations below is something a real operator has asked a real logging team for. Interviewers draw from the same well. Each variation is worth rehearsing not for its answer but for where it lands in the three-layer design: most of them touch exactly one layer, and being able to say which one is the skill being graded.
Hierarchical names: making "payments" govern "payments.checkout"
We gave loggers dotted names but flat configuration — every logger's minimum set explicitly. The natural evolution: set payments to WARN and have every payments.* logger inherit it unless something more specific overrides.
The classic resolution rule is longest-prefix wins: payments.checkout.retries looks for its own setting, then payments.checkout, then payments, then the root default. Two design consequences worth voicing. Resolution happens at logger creation (config is startup-only, so the effective level can be computed once and cached on the logger — no per-message tree walk), and the hierarchy is a configuration concept, not an object one: loggers don't hold parent pointers, they hold a resolved minimum. A flat registry with explicit per-logger settings remains perfectly defensible for a smaller shop — the trade is convention-over-configuration versus obviousness, and saying that trade out loud beats picking either silently.
Noisy call sites: sampling as a first-class feature
One log line inside a hot loop can emit ten thousand identical messages a minute — true, useless, and expensive. Operators ask for relief, and the library-level answer is per-site sampling: emit the first N occurrences, then one in every M, with a periodic summary line noting how many were skipped so the record stays honest.
Where does it land? It's a gate — so it composes with the level gate inside the logger layer, keyed by call site or by message template. The appenders never know. What's worth saying in a round: sampling trades completeness for cost visibly — the summary line is what keeps the log trustworthy, because a silently thinned log is worse than a noisy one. (Distinct problem, same instinct as the hit-rate counter in the cache chapter: observability features must themselves stay honest.)
Structured fields versus format strings
Our design carries fields as key-value pairs and lets formatters render them. The older tradition interpolates everything into the message string: "user 42 charged ₹300 in 118ms". Prose reads well in a terminal and is nearly opaque to machines; fields (user_id=42 amount=300 latency_ms=118) make every downstream pipeline — search, aggregation, alerting on latency_ms > 500 — trivial, at the cost of some human friction.
The design point: because formatters own rendering, this isn't an either/or the library must decide. The console formatter can interpolate fields into readable prose while the HTTP shipper's formatter emits them as JSON — same event, both audiences served. One caution belongs in the round: high-cardinality field values (raw user IDs on every message) are cheap for the library and expensive for whatever indexes the output downstream; a library can't fix that, but its documentation deciding what belongs in fields is part of the API design.
Testing through the seams
A design's testability is evidence about its structure, and this one testifies well. The Appender interface means tests register an in-memory capturing appender and assert on events — no stdout scraping, no temp files. The Formatter seam gets golden tests: event in, exact string out. The clock is the one dependency worth injecting explicitly (a Clock supplier in the logger's construction) so event timestamps are assertable. And the level gate has the test everyone forgets: assert that a suppressed lazy message's supplier was never invoked — that's the hot-path promise, verified rather than believed.
What startup-only configuration buys
We accepted "config resolved at startup, no runtime reload" as a requirement; it's worth knowing what that simplification purchased. Every effective level can be computed once and cached; loggers and appender lists are immutable after wiring; and there is no moment where a message races a configuration change — no half-applied config, no synchronization on the read path. Runtime reload is buildable (an atomically swapped config snapshot that new log calls pick up), but it spends complexity that this library, per its requirements, didn't need to spend. Naming what an accepted constraint bought you is a senior habit interviewers reward.
Key takeaway
Each variation lands in one layer if the layers were honest: hierarchical levels are a configuration-resolution concern cached at logger creation, sampling is another gate beside the level gate, structured-versus-prose is a formatter decision the appender split already made easy, and testability falls out of the seams. Startup-only config wasn't a limitation — it bought immutable wiring and a race-free read path, and saying what a constraint purchased is itself interview signal.