← Home
🏗Crack System Design with DSA AnimatorSystem DesignNEW
🏗 System Design›LLD Interview Prep›LLD
📝

Design a Logger

Log levels, formatters, and appenders in a handler chain — Amazon, Microsoft, and backend LLD rounds love this Chain of Responsibility classic.

📊 Easy → Medium⏱ 30–40 min🏢 Amazon · Microsoft · Oracle · Flipkart · most backend and platform SDE loops
🎯 Interview goal

Logger exposes debug/info/warn/error. Each call builds a LogRecord and passes it down a LogHandler chain. Handlers filter by LogLevel, Formatter shapes output, ConsoleAppender / FileAppender write sinks. Classic Chain of Responsibility.

📋

The interview question

“Design a logging library like log4j/slf4j.” Support log levels, pluggable appenders (console, file), and formatted messages.

Interviewers want Chain of Responsibility called out explicitly and clean separation: record creation → filter → format → append.

💡
Start with sync console logging, then add level filter and file appender. Mention async queue only if time remains.
❓

Clarifying questions (ask before coding)

1Which log levels?
Why askStandard DEBUG, INFO, WARN, ERROR — enum keeps it simple.
2Multiple appenders at once?
Why askYes — chain or fan-out to console + file.
3Thread-safe?
Why askSynchronize appenders or use lock-free queue for async.
4Structured JSON logs?
Why askSwap Formatter implementation — Template Method / Strategy hook.
5Logger hierarchy (per class/package)?
Why askOptional LoggerFactory with inherited level — extension.
✅

Requirements & assumptions (state aloud)

  • API: logger.info("msg"), logger.error("msg", exception).
  • LogRecord: level, message, timestamp, optional throwable.
  • Handlers: each has min level; passes to next handler in chain.
  • Formatters: plain text v1; JSON formatter as extension.
  • v1: synchronous, single process; no rotation or remote shipping.
🧩

High-level design

Application calls Logger → builds LogRecord → head of LogHandler chain.

Each handler: if level >= threshold, format and append; always forward to next (or stop if designed as filter-only).

ConsoleAppender writes to stdout; FileAppender writes to file with shared Formatter.

  1. LogLevel enum with ordering
  2. LogHandler — setNext + handle(record)
  3. ConsoleAppender / FileAppender + Formatter
Logger pipeline
Application → Logger → LogHandler chain → Formatter → Appenders
📐

UML class diagram (optional)

Show LogHandler interface with next pointer and two concrete appenders — classic CoR diagram.

Logger UML
Chain of Responsibility from Logger through handlers to appenders
🎨

Design patterns used (and why)

💡
Compare to log4j: Logger → Appender → Layout. Your names differ but architecture matches — say that aloud.
PatternWhereWhy (interview soundbite)
⛓️Chain of ResponsibilityLogHandler linked listEach handler decides to process and/or forward — add appenders without changing Logger.
🎯StrategyFormatter implementationsSwap text vs JSON formatting independently of sink.
🏭Factory (optional)LoggerFactoryConfigure chain once per app/module.
💻

Full solution — record & formatter

1. LogLevel & LogRecordjava
import java.time.Instant;

public enum LogLevel {
    DEBUG(0), INFO(1), WARN(2), ERROR(3);

    private final int priority;

    // Assigns numeric priority for level comparison
    LogLevel(int priority) {
        this.priority = priority;
    }

    // True if this level is at least as severe as other
    public boolean isAtLeast(LogLevel other) {
        return this.priority >= other.priority;
    }
}

public record LogRecord(
        LogLevel level,
        String message,
        Instant timestamp,
        Throwable cause) {

    // Factory for simple message without exception
    public static LogRecord of(LogLevel level, String message) {
        return new LogRecord(level, message, Instant.now(), null);
    }

    // Factory attaching stack trace source for errors
    public static LogRecord of(LogLevel level, String message, Throwable cause) {
        return new LogRecord(level, message, Instant.now(), cause);
    }
}
2. Formatterjava
public interface Formatter {
    // Converts a log record to output string (no trailing newline required)
    String format(LogRecord record);
}

public class PlainTextFormatter implements Formatter {
    // Human-readable line: timestamp LEVEL message
    @Override
    public String format(LogRecord record) {
        String base = record.timestamp() + " [" + record.level() + "] " + record.message();
        if (record.cause() == null) return base;
        return base + " | " + record.cause().toString();
    }
}

public class JsonFormatter implements Formatter {
    // Structured single-line JSON for log aggregators
    @Override
    public String format(LogRecord record) {
        return "{\"ts\":\"" + record.timestamp() + "\",\"level\":\"" + record.level()
            + "\",\"msg\":\"" + escape(record.message()) + "\"}";
    }

    // Escapes quotes in message for valid JSON
    private String escape(String s) {
        return s.replace("\\", "\\\\").replace("\"", "\\\"");
    }
}
⚙️

Full solution — handler chain & logger

3. LogHandler, appenders & chainjava
import java.io.*;
import java.nio.file.*;

public abstract class LogHandler {
    private LogHandler next;
    protected final LogLevel minLevel;
    protected final Formatter formatter;

    // Base handler with level filter and formatter
    protected LogHandler(LogLevel minLevel, Formatter formatter) {
        this.minLevel = minLevel;
        this.formatter = formatter;
    }

    // Links the next handler in the chain
    public LogHandler setNext(LogHandler next) {
        this.next = next;
        return next;
    }

    // Entry point: handle locally then forward to next
    public void handle(LogRecord record) {
        if (minLevel.isAtLeast(record.level()))
            write(formatter.format(record));
        if (next != null)
            next.handle(record);
    }

    // Subclass writes formatted line to its sink
    protected abstract void write(String formattedLine);

    // Flushes/closes resources; override in file appender
    public void close() {}
}

public class ConsoleAppender extends LogHandler {
    // Logs to standard output at or above minLevel
    public ConsoleAppender(LogLevel minLevel, Formatter formatter) {
        super(minLevel, formatter);
    }

    // Prints one formatted log line to console
    @Override
    protected void write(String formattedLine) {
        System.out.println(formattedLine);
    }
}

public class FileAppender extends LogHandler {
    private final BufferedWriter writer;

    // Opens file for append; creates parent dirs if needed
    public FileAppender(LogLevel minLevel, Formatter formatter, Path path) throws IOException {
        super(minLevel, formatter);
        Files.createDirectories(path.getParent());
        this.writer = Files.newBufferedWriter(path, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
    }

    // Writes formatted line to file with flush for durability
    @Override
    protected synchronized void write(String formattedLine) {
        try {
            writer.write(formattedLine);
            writer.newLine();
            writer.flush();
        } catch (IOException e) {
            System.err.println("FileAppender failed: " + e.getMessage());
        }
    }

    // Closes underlying file writer
    @Override
    public void close() {
        try { writer.close(); } catch (IOException ignored) {}
    }
}
4. Logger & demojava
public class Logger {
    private final String name;
    private final LogHandler chainHead;
    private LogLevel level = LogLevel.DEBUG;

    // Binds logger name to the first handler in the chain
    public Logger(String name, LogHandler chainHead) {
        this.name = name;
        this.chainHead = chainHead;
    }

    // Drops messages below configured logger level
    public void setLevel(LogLevel level) {
        this.level = level;
    }

    // Emits DEBUG record if enabled
    public void debug(String message) {
        log(LogLevel.DEBUG, message, null);
    }

    // Emits INFO record if enabled
    public void info(String message) {
        log(LogLevel.INFO, message, null);
    }

    // Emits WARN record if enabled
    public void warn(String message) {
        log(LogLevel.WARN, message, null);
    }

    // Emits ERROR record with optional throwable
    public void error(String message, Throwable cause) {
        log(LogLevel.ERROR, message, cause);
    }

    // Builds LogRecord and dispatches into handler chain
    private void log(LogLevel msgLevel, String message, Throwable cause) {
        if (!msgLevel.isAtLeast(level)) return;
        LogRecord record = cause == null
            ? LogRecord.of(msgLevel, "[" + name + "] " + message)
            : LogRecord.of(msgLevel, "[" + name + "] " + message, cause);
        chainHead.handle(record);
    }
}

public class LoggerDemo {
    // Chains console (INFO+) and file (ERROR+) appenders
    public static void main(String[] args) throws Exception {
        Formatter fmt = new PlainTextFormatter();
        LogHandler console = new ConsoleAppender(LogLevel.INFO, fmt);
        LogHandler file = new FileAppender(LogLevel.ERROR, fmt, Path.of("logs/app.log"));
        console.setNext(file);

        Logger log = new Logger("OrderService", console);
        log.debug("won't print"); // below logger level default passes but console filters DEBUG if min INFO
        log.info("order created");
        log.error("payment failed", new RuntimeException("timeout"));
        file.close();
    }
}
🎤

While you code — questions interviewers ask

🚀

Extensions & follow-ups

  • Async appender with bounded queue and drop policy.
  • MDC (mapped diagnostic context) for traceId in Formatter.
  • Remote appender (Kafka, CloudWatch) as another handler.
  • Logger hierarchy inheriting levels from parent package logger.
⚠️

Common mistakes (fail signals)

  • God class Logger that both formats and writes file — no separation.
  • Forgetting to forward to next handler — breaks multi-appender setup.
  • No level ordering — string compare on enum names fails.
  • Unsynchronized FileAppender corrupting lines under concurrency.
🧾

60-second cheat sheet

PatternChain of Responsibility
FlowLogger → LogRecord → Handler chain → Format → Append
TypesLogLevel, LogRecord, Logger, LogHandler, Formatter
AppendersConsoleAppender, FileAppender
Scale-upAsync queue · rotation · JSON Formatter
🔗

More LLD problems