Requirements checklist
Ask, one at a time, absorbing each answer:
□ levels + WHERE filtering lives two gates (logger + destination)? □ destinations, several at once? forces a real dispatch layer □ formats differ per destination? forces formatter/appender split □ structured fields — bindable request-scoped context objects to a logger, or per-message only? □ cost of a SUPPRESSED message? dictates order of operations in log() □ threads + ordering promises? per-thread order at one destination □ config: startup or runtime? startup-only buys immutable wiring
The core model
Logger name, minLevel (gate #1), boundFields (immutable),
shared appender list
LogEvent timestamp, level, loggerName, message, merged fields,
threadId — immutable, built ONCE, after gate #1
Appender minLevel (gate #2) + append(event); owns its Formatter
Formatter event -> text; any format pairs with any destination
log() flow gate FIRST -> merge fields (per-call wins, one line)
-> build event -> for each appender: filter, format, write
with(fields) returns a NEW child logger — context never mutates a
shared logger
lazy form debug(supplier) — supplier never runs if suppressed
Invariants: event immutable after construction · suppressed call allocates nothing · the field-merge collision rule exists in exactly one line · appenders serialize their own writes.
Principles demonstrated (name them at the decision)
- Single responsibility — formatting and writing are different jobs; the split is why destination #4 costs one class.
- Open/closed — new destination = new Appender; nothing else touched.
- Immutability as thread-safety — events shared freely across appenders; child loggers make context aliasing unrepresentable.
Complexity facts
suppressed message O(1) compare, ZERO allocation — because the gate
precedes construction (placement, not luck)
emitted message O(fields) merge once + O(appenders) dispatch
+ per-appender formatting (only past its gate)
per-destination synchronized write = ordering promise; contention
only when many threads hit one appender
What earns points, per report dimension
- Requirements & interface — the two-gates question asked; destinations, fields, hot-path cost, and ordering promises pinned before design.
- Core design & invariants — three layers each standing on a requirement; event as immutable value built once; gate-before-construction stated as a rule, not an accident; child-logger context with a defined merge order.
- Extension probe — the round will move the requirements; points come from locating the change in one layer and saying why the other layers were protected by the seams.
- Complexity honesty — the suppressed-message cost claimed with its placement justification; emitted-path costs itemized per layer.
- Communication — layering justified from requirements as you draw it, rejections narrated, probes answered directly.