Overview
Designing a logging framework (like a mini Log4j or SLF4J) is a popular LLD question. Core concepts: a Logger per name, log levels (DEBUG < INFO < WARN < ERROR) with filtering, log records with timestamp, level, message, and context, formatters that turn records into text or JSON, and appenders (sinks) that write to console, files, or remote systems.
The design showcases many patterns: Singleton or factory for logger lookup, Strategy for formatters, Observer or Composite for multiple appenders, Chain of Responsibility for level filtering and hierarchical loggers, and asynchronous appenders with a queue for performance. Thread safety and not blocking the caller are key concerns.
Reporters (loggers) file stories with an urgency level. Editors (filters) drop minor stories when busy. Layout (formatters) shapes each story, and distribution (appenders) sends it to print, web, and radio at the same time.
When to use it
- Interview prompt: 'Design a logger / logging library'.
- Understanding how SLF4J, Log4j, and Winston work.
- Building structured logging for a platform.
Where it shows up in interviews
Recognize it when: levels, multiple sinks, formats.
- Design a logging framework
- Design an audit trail library
Where it is used in real software
Loggers, levels, appenders, layouts, and async appenders with LMAX Disruptor.
A facade decoupling application code from logging implementations.
Node.js loggers with transports and JSON output.
Key terms
- Logger
- Named entry point (com.shop.orders).
- Level
- Severity threshold for filtering.
- Appender / sink
- Destination for records.
- Formatter / layout
- Converts records to output text.
- Async appender
- Queues records and writes on a background thread.
How it works, step by step
- 1Define LogRecord
time, level, logger name, message, fields, error.
- 2Logger checks level
Cheap early return when disabled.
- 3Dispatch to appenders
Each with its own formatter and minimum level.
- 4Make appenders pluggable
Console, file, rolling file, HTTP.
- 5Add async and thread safety
Bounded queue and a writer thread.
Routing records
Logger level INFO; console appender INFO+, file appender WARN+, JSON formatter
| Call | Logger passes? | Console | File |
|---|---|---|---|
| debug('cache miss') | No | - | - |
| info('order placed') | Yes | Written | - |
| warn('slow payment') | Yes | Written | Written |
| error('db down', err) | Yes | Written | Written with stack trace |
NOWCall: debug('cache miss') | Logger passes?: No | Console: - | File: -
Filtering at the logger and at each appender keeps output targeted and cheap.
Implementation
import java.time.Instant;import java.util.*;import java.util.concurrent.*; public enum Level { DEBUG, INFO, WARN, ERROR } public record LogRecord(Instant time, Level level, String logger, String message, Map<String, Object> fields, Throwable error) {} public interface Formatter { String format(LogRecord r); } public interface Appender { void append(LogRecord r); } public final class JsonFormatter implements Formatter { public String format(LogRecord r) { StringBuilder sb = new StringBuilder("{\"ts\":\"").append(r.time()).append("\",\"level\":\"").append(r.level()) .append("\",\"logger\":\"").append(r.logger()).append("\",\"msg\":\"").append(r.message()).append('"'); r.fields().forEach((k, v) -> sb.append(",\"").append(k).append("\":\"").append(v).append('"')); return sb.append('}').toString(); }} public final class ConsoleAppender implements Appender { private final Level min; private final Formatter formatter; public ConsoleAppender(Level min, Formatter formatter) { this.min = min; this.formatter = formatter; } public void append(LogRecord r) { if (r.level().compareTo(min) >= 0) System.out.println(formatter.format(r)); }} public final class AsyncAppender implements Appender, AutoCloseable { private final BlockingQueue<LogRecord> queue = new ArrayBlockingQueue<>(10_000); private final Thread writer; public AsyncAppender(Appender delegate) { writer = new Thread(() -> { try { while (true) delegate.append(queue.take()); } catch (InterruptedException ignored) {} }, "log-writer"); writer.setDaemon(true); writer.start(); } public void append(LogRecord r) { queue.offer(r); } // drop when full rather than block callers public void close() { writer.interrupt(); }} public final class Logger { private final String name; private volatile Level level; private final List<Appender> appenders; Logger(String name, Level level, List<Appender> appenders) { this.name = name; this.level = level; this.appenders = appenders; } public void info(String msg, Map<String, Object> fields) { log(Level.INFO, msg, fields, null); } public void error(String msg, Throwable e) { log(Level.ERROR, msg, Map.of(), e); } private void log(Level l, String msg, Map<String, Object> fields, Throwable e) { if (l.compareTo(level) < 0) return; // cheap filter LogRecord r = new LogRecord(Instant.now(), l, name, msg, fields, e); for (Appender a : appenders) a.append(r); }} public final class LoggerFactory { private static final ConcurrentHashMap<String, Logger> LOGGERS = new ConcurrentHashMap<>(); private static final List<Appender> APPENDERS = List.of(new AsyncAppender(new ConsoleAppender(Level.INFO, new JsonFormatter()))); public static Logger get(String name) { return LOGGERS.computeIfAbsent(name, n -> new Logger(n, Level.INFO, APPENDERS)); }}Complexity and performance
Must be near-free.
Async moves I/O off the caller.
Trade-offs
Sync guarantees delivery order and durability; async protects latency but can drop records under pressure.
Dropping protects the application; blocking guarantees logs but can stall requests.
Variants and related techniques
Child loggers inherit levels and appenders from parents (com.shop inherits from com).
Rotate by size or date and retain N files.
Common mistakes
- Building strings before checking the level.
Fix: Check level first or use lazy/parameterized messages.
- Appenders that throw into the caller.
Fix: Catch and report appender errors internally.
- Non-thread-safe file writes.
Fix: Single writer thread or synchronized appender.
Interview questions
Which design patterns appear in a logging framework?
Factory/Singleton for logger lookup, Strategy for formatters, Observer-like fan-out to multiple appenders, Chain of Responsibility or hierarchy for level filtering, Decorator for async or buffered appenders, and Facade (SLF4J) over implementations.
How do you keep logging from slowing the application?
Cheap level checks, lazy message construction, asynchronous appenders with bounded queues, batching writes, and dropping low-priority logs when overloaded.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Logger with levels and two appenders | Easy | Filtering. |
| Async rolling file appender | Hard | Concurrency and rotation. |