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.
🟠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.
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.
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
Appenderinterface withappend(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
Formatterinterface — 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
- Chain of Responsibility · Strategy · Singleton — the patterns used here
- Design a Notification Service — next up, another multi-channel fan-out
- Message Queues & Kafka — the async-logging backbone
- ← The complete System Design guide (hub)