About Us Contact Us Write for Us Advertise
Home > System Design > Low-Level Design > Design a Logging Framework (Low-Level Design)
System Design › Low-Level Design

Design a Logging Framework (Low-Level Design)

Design a logging framework (Log4j-style) as a showcase of patterns: level filtering via Chain of Responsibility, pluggable Appenders for destinations, Strategy formatters, a Singleton logger, plus thread-safety and async logging.

Shiv Pandey
Shiv Pandey
Oct 29, 2026 | 3 views
Design a Logging Framework (Low-Level Design)

🟠 Low-Level Design · Case Study · Senior

"Design a logging framework" (think Log4j / SLF4J) is a lovely LLD problem because a good answer is almost entirely patterns. It asks you to route a log message through levels, filters, formatters, and multiple destinations — each of which maps cleanly onto a classic design pattern. Nail the patterns and you nail the interview. Let's design it.

The approach in one line: a Logger creates a LogMessage at some level; the message flows through a Chain of Responsibility of level-based handlers, each writing to a pluggable Appender (console / file / DB) using a pluggable Strategy formatter.

Step 1 — Requirements

  • Functional: log at levels DEBUG < INFO < WARN < ERROR; only emit messages at or above a configured threshold; write to multiple destinations (console, file, remote); configurable message format (timestamp, level, thread, text).
  • Non-functional: easily extensible (add a new level, destination, or format without touching callers), low overhead, thread-safe, ideally non-blocking.

Step 2 — The classes

  • LogLevel (enum: DEBUG, INFO, WARN, ERROR) — ordered so we can compare "at or above".
  • LogMessage — level, text, timestamp, thread, logger name.
  • Logger — the front door callers use (logger.info("...")); holds the configured level + appenders.
  • LogHandler (Chain of Responsibility) — each handles one level band and passes on.
  • Appender (interface) → ConsoleAppender, FileAppender, DatabaseAppender — where it goes.
  • Formatter (Strategy) → each defines how the line looks.
Logger DEBUG handler INFO handler ERROR handler Formatter Console File Database Chain of Responsibility (levels) Appenders

Step 3 — Level filtering = Chain of Responsibility

🟠 The Chain of Responsibility is the natural fit for level handling: each handler knows its level, decides whether to act, and passes the message along. Set the chain's threshold and everything below it is silently dropped.

abstract class LogHandler {                    // Chain of Responsibility
    protected LogHandler next;
    protected LogLevel level;
    void log(LogMessage msg) {
        if (msg.level.ordinal() >= level.ordinal()) write(msg);  // at or above threshold
        if (next != null) next.next(msg);          // pass down the chain
    }
    protected abstract void write(LogMessage msg);
}

Step 4 — Destinations = Appender, format = Strategy

🔴 Two more clean pattern choices:

  • Appender (where): an Appender interface with append(line) — ConsoleAppender, FileAppender, DatabaseAppender. Adding a new sink (e.g. a Kafka appender that ships logs to a queue) is a new class. A logger can hold a list of appenders, so one message fans out to several destinations — that's an Observer-style broadcast.
  • Formatter (how) = Strategy: a Formatter interface — PlainTextFormatter, JsonFormatter — so the same message can be rendered differently per appender without changing anything else.

Step 5 — The senior nuances

🔴 What separates a good answer from a great one:

  • The Logger is usually a Singleton (or obtained via a factory like LoggerFactory.getLogger(Class)) — one shared, configured instance.
  • Thread-safety: many threads log at once, so appenders must synchronize writes (or use thread-safe sinks). Mention it explicitly.
  • Async logging: writing to disk/DB on the caller's thread is slow. Put messages on a queue and let a background worker flush them — high-throughput frameworks do exactly this (the same producer/consumer idea as message queues). Call out the trade-off: you can lose in-flight logs on a crash.
  • Level check first: guard expensive string building behind if (logger.isDebugEnabled()) so disabled logs cost almost nothing.

The interview-ready walkthrough

"I'd expose a Logger (a Singleton via a factory) that builds a LogMessage with a level. Level filtering is a Chain of Responsibility — each handler emits at-or-above its threshold and passes on. Where logs go is an Appender interface (console/file/DB), and a logger holds a list of them so one message fans out. How they look is a Strategy Formatter (plain/JSON). For production I'd make appenders thread-safe and add async logging via a queue + background worker, guarding expensive calls behind a level check." That's four patterns and the real-world concerns in one answer."

What to read next

← Design an ATM · Design a notification service →

Related Articles

How to Approach a Low-Level Design (LLD) Interview: The Framework
System Design › Low-Level Design

How to Approach a Low-Level Design (LLD) Interview: The Framework

Design an ATM (Low-Level Design)
System Design › Low-Level Design

Design an ATM (Low-Level Design)

Design Tic-Tac-Toe (Low-Level Design / OOD)
System Design › Low-Level Design

Design Tic-Tac-Toe (Low-Level Design / OOD)

Design an LRU Cache (O(1) get & put) — Low-Level Design
System Design › Low-Level Design

Design an LRU Cache (O(1) get & put) — Low-Level Design