REUSABLE COMPONENT DESIGN / OBJECT DESIGN BRIEF

Logging framework design

Designing a logging framework (like a mini Log4j or SLF4J) is a popular LLD question.

IntermediatePhase 08 / Topic 6 of 7ResponsibilitiesCollaborationsExtensibility
01

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.

A newsroom

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.

02

When to use it

  • Interview prompt: 'Design a logger / logging library'.
  • Understanding how SLF4J, Log4j, and Winston work.
  • Building structured logging for a platform.
03

Where it shows up in interviews

Framework design

Recognize it when: levels, multiple sinks, formats.

  • Design a logging framework
  • Design an audit trail library
04

Where it is used in real software

Log4j2 / Logback

Loggers, levels, appenders, layouts, and async appenders with LMAX Disruptor.

SLF4J

A facade decoupling application code from logging implementations.

Pino and Winston

Node.js loggers with transports and JSON output.

05

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.
06

How it works, step by step

  1. 1
    Define LogRecord

    time, level, logger name, message, fields, error.

  2. 2
    Logger checks level

    Cheap early return when disabled.

  3. 3
    Dispatch to appenders

    Each with its own formatter and minimum level.

  4. 4
    Make appenders pluggable

    Console, file, rolling file, HTTP.

  5. 5
    Add async and thread safety

    Bounded queue and a writer thread.

07

Routing records

Logger level INFO; console appender INFO+, file appender WARN+, JSON formatter

Step 1 / 4
CallLogger passes?ConsoleFile
debug('cache miss')No--
info('order placed')YesWritten-
warn('slow payment')YesWrittenWritten
error('db down', err)YesWrittenWritten 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.

08

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)); }}
09

Complexity and performance

Disabled level checkO(1)

Must be near-free.

Enabled logO(appenders + format)

Async moves I/O off the caller.

10

Trade-offs

Sync vs async appenders

Sync guarantees delivery order and durability; async protects latency but can drop records under pressure.

Drop vs block when full

Dropping protects the application; blocking guarantees logs but can stall requests.

11

Variants and related techniques

Hierarchical loggers

Child loggers inherit levels and appenders from parents (com.shop inherits from com).

Rolling file appender

Rotate by size or date and retain N files.

12

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.

13

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.

14

Practice problems

ProblemDifficultyWhat it trains
Logger with levels and two appendersEasyFiltering.
Async rolling file appenderHardConcurrency and rotation.