DEV Community

Dev Cookies
Dev Cookies

Posted on

Logger System LLD (Java)

Problem Statement

Design a Logger Framework that supports:

  • Multiple Log Levels

    • DEBUG
    • INFO
    • WARN
    • ERROR
    • FATAL
  • Multiple Output Destinations

    • Console
    • File
    • Database
    • Kafka (future)
    • ElasticSearch (future)
  • Configurable Minimum Log Level

  • Thread Safe

  • Easy to extend


Functional Requirements

  • Log message
  • Timestamp
  • Log Level
  • Destination
  • Format message

Non Functional Requirements

  • Extensible
  • Low Coupling
  • High Cohesion
  • Open Closed Principle
  • Thread Safe
  • Singleton Logger

Interview Expectation

Application
      │
      ▼
 Logger (Singleton)
      │
      ▼
 LogManager
      │
      ▼
Formatter
      │
      ▼
Appender
 ├── ConsoleAppender
 ├── FileAppender
 ├── DatabaseAppender
Enter fullscreen mode Exit fullscreen mode

Design Patterns Used

Pattern Why
Singleton One Logger instance
Strategy Different Appenders
Factory Create appenders
Builder Build LogMessage
Chain of Responsibility (Optional) Multiple appenders
Dependency Injection Loose coupling

Step 1: Log Level

public enum LogLevel {

    DEBUG(1),
    INFO(2),
    WARN(3),
    ERROR(4),
    FATAL(5);

    private final int priority;

    LogLevel(int priority) {
        this.priority = priority;
    }

    public int getPriority() {
        return priority;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Log Message

import java.time.LocalDateTime;

public class LogMessage {

    private final LogLevel level;
    private final String message;
    private final LocalDateTime timestamp;

    public LogMessage(LogLevel level, String message) {
        this.level = level;
        this.message = message;
        this.timestamp = LocalDateTime.now();
    }

    public LogLevel getLevel() {
        return level;
    }

    public String getMessage() {
        return message;
    }

    public LocalDateTime getTimestamp() {
        return timestamp;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Formatter

public interface LogFormatter {

    String format(LogMessage message);

}
Enter fullscreen mode Exit fullscreen mode

Default Formatter

public class DefaultFormatter implements LogFormatter {

    @Override
    public String format(LogMessage message) {

        return String.format(
                "[%s] [%s] %s",
                message.getTimestamp(),
                message.getLevel(),
                message.getMessage());

    }

}
Enter fullscreen mode Exit fullscreen mode

Step 4: Appender

public interface Appender {

    void append(LogMessage message);

}
Enter fullscreen mode Exit fullscreen mode

Console Appender

public class ConsoleAppender implements Appender {

    private final LogFormatter formatter;

    public ConsoleAppender(LogFormatter formatter) {
        this.formatter = formatter;
    }

    @Override
    public void append(LogMessage message) {
        System.out.println(formatter.format(message));
    }
}
Enter fullscreen mode Exit fullscreen mode

File Appender

import java.io.FileWriter;
import java.io.IOException;

public class FileAppender implements Appender {

    private final LogFormatter formatter;

    public FileAppender(LogFormatter formatter) {
        this.formatter = formatter;
    }

    @Override
    public void append(LogMessage message) {

        try(FileWriter writer = new FileWriter("app.log", true)) {

            writer.write(formatter.format(message));
            writer.write("\n");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Logger

import java.util.List;

public class Logger {

    private static volatile Logger instance;

    private final List<Appender> appenders;

    private LogLevel minimumLevel;

    private Logger(List<Appender> appenders,
                   LogLevel minimumLevel) {

        this.appenders = appenders;
        this.minimumLevel = minimumLevel;
    }

    public static Logger getInstance(List<Appender> appenders,
                                     LogLevel level) {

        if (instance == null) {

            synchronized (Logger.class) {

                if (instance == null) {

                    instance = new Logger(appenders, level);

                }

            }

        }

        return instance;
    }

    public void log(LogLevel level,
                    String message) {

        if(level.getPriority() < minimumLevel.getPriority())
            return;

        LogMessage logMessage =
                new LogMessage(level, message);

        for(Appender appender : appenders)
            appender.append(logMessage);

    }

}
Enter fullscreen mode Exit fullscreen mode

Client

import java.util.List;

public class Main {

    public static void main(String[] args) {

        LogFormatter formatter =
                new DefaultFormatter();

        Appender console =
                new ConsoleAppender(formatter);

        Appender file =
                new FileAppender(formatter);

        Logger logger =
                Logger.getInstance(
                        List.of(console, file),
                        LogLevel.INFO);

        logger.log(LogLevel.INFO,
                "Application Started");

        logger.log(LogLevel.DEBUG,
                "Debug Message");

        logger.log(LogLevel.ERROR,
                "Database Down");

    }

}
Enter fullscreen mode Exit fullscreen mode

Output

[2026-07-09T10:30] [INFO] Application Started

[2026-07-09T10:31] [ERROR] Database Down
Enter fullscreen mode Exit fullscreen mode

(DEBUG ignored)


UML

                    +------------------+
                    |      Logger      |
                    +------------------+
                    | minimumLevel     |
                    | appenders        |
                    +------------------+
                          |
          -------------------------------
          |                             |
          ▼                             ▼
 +----------------+             +----------------+
 |   Appender     |             | LogFormatter   |
 +----------------+             +----------------+
 | append()       |             | format()       |
 +----------------+             +----------------+
      ▲
      │
-----------------------------
|             |             |
▼             ▼             ▼
Console    File       Database
Appender    Appender    Appender

                ▲
                |
          +--------------+
          | LogMessage   |
          +--------------+
          | level        |
          | timestamp    |
          | message      |
          +--------------+
Enter fullscreen mode Exit fullscreen mode

SOLID Principles

Principle Application
SRP LogMessage, Appender, Formatter, Logger each have a single responsibility.
OCP Add new appenders or formatters without modifying existing classes.
LSP Any Appender implementation can replace another.
ISP Small focused interfaces (Appender, LogFormatter).
DIP Logger depends on abstractions rather than concrete implementations.

Common Interview Follow-up Questions

  1. How would you support asynchronous logging?
  • Introduce a BlockingQueue<LogMessage> and a dedicated worker thread or thread pool that consumes log events and dispatches them to appenders.
  1. How would you prevent logging from blocking application threads?
  • Use non-blocking queues, batching, and asynchronous appenders with configurable backpressure policies.
  1. How would you support log rotation?
  • Create a RollingFileAppender that rotates files based on size or time (daily/hourly) and manages retention.
  1. How would you support multiple log formats?
  • Implement additional LogFormatter strategies such as JsonFormatter, XmlFormatter, or custom templates.
  1. How would you enable configuration from a properties or YAML file?
  • Build a LoggerConfiguration component and an AppenderFactory to instantiate appenders based on external configuration.
  1. How would you make the logger thread-safe?
  • Keep shared state immutable where possible, use thread-safe collections if appenders are modified at runtime, and ensure appenders handle concurrent writes correctly.
  1. How would you add contextual information (e.g., request ID, user ID)?
  • Introduce a Mapped Diagnostic Context (MDC) using ThreadLocal to automatically enrich each LogMessage.
  1. How would you support filtering?
  • Add a LogFilter abstraction (Strategy pattern) to filter messages by package, marker, or custom predicates before dispatching.

Possible Extensions

  • AsyncLogger
  • RollingFileAppender
  • DatabaseAppender
  • KafkaAppender
  • ElasticSearchAppender
  • JsonFormatter
  • XMLFormatter
  • LoggerFactory
  • LoggerConfiguration
  • LogFilter
  • MDC (Mapped Diagnostic Context)
  • Rate limiting and sampling
  • Dynamic log level updates
  • Metrics for dropped/processed log events

This design demonstrates strong OOP fundamentals, SOLID principles, and extensibility while remaining concise enough to explain in a typical 30–45 minute LLD interview.

Top comments (0)