LLD Hub
lldchain-of-responsibilitysingletonobserver

How to Design a Logging Framework | LLD Interview Guide

Design a flexible logging system with multiple handlers, formatters, and log levels. Chain of Responsibility, Singleton, and Observer patterns explained.

20 April 2025·7 min read

Practice this problem

Logger / Logging Framework — get AI-scored feedback on your solution

Solve it →

Designing a Logging Framework tests your knowledge of Chain of Responsibility, Singleton, and the Template Method pattern. It's a solid beginner-to-intermediate LLD problem that appears in SDE-1 interviews and as a warm-up in senior interviews.

Core Entities

  • Logger — named logger, has a minimum log level and list of handlers
  • LogRecord — level, message, timestamp, logger name, context
  • Handler — processes a LogRecord (Console, File, Database, Remote)
  • Formatter — formats a LogRecord into a string
  • LogLevel — DEBUG(10), INFO(20), WARNING(30), ERROR(40), CRITICAL(50)

Chain of Responsibility for Handlers

abstract class LogHandler {
  protected next: LogHandler | null = null;
  protected level: LogLevel;

  setNext(handler: LogHandler): LogHandler { this.next = handler; return handler; }

  handle(record: LogRecord): void {
    if (record.level >= this.level) this.emit(record); // This handler processes it
    this.next?.handle(record);                          // Pass to next handler too
  }

  abstract emit(record: LogRecord): void;
}

class ConsoleHandler extends LogHandler {
  emit(record: LogRecord) {
    console.log(this.formatter.format(record));
  }
}

class FileHandler extends LogHandler {
  emit(record: LogRecord) {
    this.fileWriter.append(this.logFile, this.formatter.format(record));
  }
}

Logger — Singleton per Name

class LoggerRegistry {
  private static loggers = new Map<string, Logger>();

  static getLogger(name: string): Logger {
    if (!this.loggers.has(name)) {
      const logger = new Logger(name);
      // Attach default handlers from root config
      logger.addHandler(new ConsoleHandler(LogLevel.DEBUG));
      this.loggers.set(name, logger);
    }
    return this.loggers.get(name)!;
  }
}

class Logger {
  private handlers: LogHandler[] = [];
  private level: LogLevel = LogLevel.DEBUG;

  log(level: LogLevel, message: string, context?: object) {
    if (level < this.level) return; // Filter below configured level
    const record = new LogRecord(level, message, this.name, context);
    this.handlers.forEach(h => h.handle(record));
  }
  debug(msg: string)    { this.log(LogLevel.DEBUG, msg); }
  info(msg: string)     { this.log(LogLevel.INFO, msg); }
  error(msg: string)    { this.log(LogLevel.ERROR, msg); }
}

Formatter — Template Method

abstract class LogFormatter {
  format(record: LogRecord): string {
    return `[${this.formatTimestamp(record.timestamp)}] ${this.formatLevel(record.level)} ${this.formatMessage(record)}`;
  }
  abstract formatTimestamp(ts: Date): string;
  abstract formatLevel(level: LogLevel): string;
  abstract formatMessage(record: LogRecord): string;
}
class JSONFormatter extends LogFormatter {
  formatTimestamp(ts: Date) { return ts.toISOString(); }
  formatLevel(level: LogLevel) { return LogLevel[level]; }
  formatMessage(record: LogRecord) { return JSON.stringify({ msg: record.message, ctx: record.context }); }
}

Ready to practice?

Submit your solution and get AI-scored feedback on OOP, SOLID principles, design patterns, and code quality.

Solve Logger / Logging Framework