Compile Ready
All low level design problems
Low Level Design/Intermediate/Advanced Systems

Design a Logging Framework

Levels, appenders, and a handler chain — a Chain-of-Responsibility showcase with async, thread-safe sinks.

Intermediate 45m interview 11m read High frequency Popularity 84
Chain of Responsibility Singleton Strategy Observer Amazon Microsoft Oracle

Problem Statement

Design the object model for a logging framework that application code can use to emit diagnostics at different severities. The framework should support DEBUG, INFO, WARN, and ERROR levels, named loggers, level filtering, a chain of handlers, pluggable formatters, and pluggable sinks such as console and file output.

The interview focus is the class model: where filtering lives, how messages move through a chain, how sinks are added without changing core logging code, and how the same design can evolve into async logging for production workloads.

Business context

Logging frameworks sit on every critical path at companies like Amazon, Microsoft, and Oracle. They must be easy for application teams to call, cheap when a message is filtered out, and extensible enough to add structured formats, file rotation, remote collectors, or async buffering later.

A strong LLD answer shows that logging is not just printing text. It is a pipeline: caller API → level filter → log event → handler chain → formatter strategy → observer appenders.

Functional Requirements

  • Expose named loggers through a central manager so different application modules can request their own logger.

  • Support the four levels DEBUG, INFO, WARN, and ERROR with severity ordering.

  • Filter messages below the configured minimum level before creating or publishing a log event.

  • Route accepted messages through a Chain of Responsibility made of log-level handlers.

  • Format each accepted LogMessage through a pluggable Formatter strategy.

  • Publish the formatted message to one or more pluggable Appender sinks, including console and file sinks.

  • Provide convenience methods debug, info, warn, and error in addition to a generic log method.

  • Keep the sink abstraction compatible with an async appender that can enqueue work without changing Logger.

Non-Functional Requirements

Low overhead on filtered logs

A message below the logger's minimum level should return immediately. It should not allocate a LogMessage, run the handler chain, format text, or touch any sink.

Thread-safe shared loggers

LogManager must safely return the same named logger to concurrent callers. Logger configuration and appender iteration must not corrupt shared state.

Extensibility

New formatters, appenders, and async wrappers should be additive classes behind existing interfaces rather than edits to the logging flow.

Failure isolation

A sink failure should be isolated at the appender boundary. The reference FileAppender converts I/O errors into a clear runtime failure rather than silently losing logs.

Deterministic message model

A log event should capture its level, logger name, text, and creation time once so every appender sees the same event content.

Requirement Clarification

QDo we need hierarchical package loggers like **com.app.service** inheriting from **com.app**?

Not in the base design. We model named loggers returned by LogManager. Hierarchical inheritance is a documented extension.

QShould logging calls be async by default?

For the reference design, Logger publishes synchronously to keep the core pipeline readable. Async logging is supported by the Appender seam: a production AsyncAppender can enqueue formatted messages and flush on a worker.

QCan a logger write to multiple destinations?

Yes. Logger owns a list of appenders and publishes the same formatted message to each one, such as console plus file.

QCan users change the output format?

Yes. Formatter is an interface. SimpleFormatter is the default, and structured or JSON formatters can implement the same method.

QWhat happens when a message level does not match the first handler?

The LogHandler passes the event to the next handler until a matching LevelLogHandler publishes it or the chain ends.

UML Class Diagram

Rendering diagram…
The design has three seams: **LogHandler** forms the responsibility chain, **Formatter** chooses rendering, and **Appender** observers receive the rendered output.

Sequence Diagram

Rendering diagram…
Rejected logs stop at the level check. Accepted logs pass through the handler chain, then one formatted string is broadcast to every configured appender.

Entity Identification

LogManager

Singleton registry for named Logger instances. It uses a concurrent map so repeated calls for the same name return the same logger safely.

loggers

Logger

Facade used by application code. It owns the minimum level, handler chain, formatter, and appender list, and coordinates the full logging pipeline.

nameappendershandlerChainminimumLevelformatter

LogLevel

Severity enum with explicit numeric ordering. isAtLeast is the single source of truth for level filtering.

DEBUGINFOWARNERRORseverity

LogMessage

Immutable event object created only after filtering succeeds. It captures logger name, level, text, and timestamp.

loggerNameleveltextcreatedAt

LogHandler

Abstract chain node. handle either publishes the message when this handler accepts it or forwards to the next handler.

next

LevelLogHandler

Concrete handler for one severity. defaultChain wires DEBUG → INFO → WARN → ERROR handlers in order.

handledLevel

Formatter

Rendering strategy interface. It turns a LogMessage into text without knowing where that text will be written.

format(message)

SimpleFormatter

Default formatter that emits timestamp, level, logger name, and message text in a predictable human-readable form.

DateTimeFormatter

Appender

Sink observer interface. Each appender receives the rendered log line and decides where or how to write it.

append(formattedMessage)

ConsoleAppender

Console sink that writes formatted messages to standard output.

append(formattedMessage)

FileAppender

File sink that appends each formatted message to a path using UTF-8 and synchronizes writes for thread safety.

path

Design Patterns Used

Chain of Responsibility

LogHandler owns the chain link and LevelLogHandler owns the level match. Logger sends a message to the chain without switching on every level itself.

Singleton

LogManager uses the holder idiom to expose exactly one registry of named loggers across the process.

Strategy

Formatter is the rendering strategy and Appender is the sink strategy. New text formats and destinations are added behind interfaces.

Observer

Logger.publish broadcasts one formatted event to every registered Appender. Console, file, and future async appenders observe the same stream.

Step-by-Step Design

  1. 1Model severity as an ordered enum

    Give each LogLevel an explicit severity number so filtering is stable even if enum declaration order changes later.

    public enum LogLevel {
        DEBUG(10), INFO(20), WARN(30), ERROR(40);
    
        public boolean isAtLeast(LogLevel minimum) {
            return severity >= minimum.severity;
        }
    }
  2. 2Capture accepted events as immutable messages

    LogMessage.create stamps the event with the current time after filtering. Every formatter and appender sees the same logger name, level, text, and timestamp.

  3. 3Build a level handler chain

    Each LevelLogHandler is responsible for exactly one level. The default chain makes level routing explicit without a switch inside Logger.

    public static LogHandler defaultChain() {
        LogHandler debug = new LevelLogHandler(LogLevel.DEBUG);
        debug.linkWith(new LevelLogHandler(LogLevel.INFO))
            .linkWith(new LevelLogHandler(LogLevel.WARN))
            .linkWith(new LevelLogHandler(LogLevel.ERROR));
        return debug;
    }
  4. 4Filter first, then publish

    Logger.log rejects below-threshold messages before allocation. Accepted messages go into the handler chain, which calls back into publish.

    public void log(LogLevel level, String text) {
        if (!level.isAtLeast(minimumLevel)) {
            return;
        }
        handlerChain.handle(LogMessage.create(name, level, text), this);
    }
  5. 5Separate formatting from delivery

    Logger.publish asks the current Formatter for one rendered string, then iterates over the appender list. Output policy is not hard-coded into the logger.

  6. 6Centralize logger lookup in a singleton manager

    LogManager.getInstance exposes the process-wide registry, while getLogger uses atomic map insertion so named loggers are reused safely.

  7. 7Keep async logging as an appender-level extension

    Because Logger depends only on Appender, a future async implementation can enqueue the formatted line and return quickly. Backpressure, flushing, and worker lifecycle stay outside the core logger.

Complete Java Implementation

Loading…

Explanation of Every Class

LogLevel

Enum for DEBUG, INFO, WARN, and ERROR. The numeric severity makes isAtLeast the single filter rule used by Logger.

LogMessage

Immutable event value. Its static create factory captures the current Instant and stores logger name, level, and text behind getters.

Formatter

Strategy interface for rendering a LogMessage. It keeps the logger independent from text shape, structured output, or locale choices.

SimpleFormatter

Default Formatter implementation. It uses an ISO offset timestamp and emits level, logger name, and text as one human-readable line.

Appender

Sink interface for already formatted text. Console, file, remote, buffered, and async destinations can all implement append.

ConsoleAppender

Simple Appender that writes the formatted line to standard output. It is the default sink added to every new Logger.

FileAppender

File-backed Appender. It appends UTF-8 bytes to a configured path, creates the file when needed, synchronizes writes, and surfaces I/O failure as IllegalStateException.

LogHandler

Abstract Chain of Responsibility node. linkWith wires the chain, and final handle either publishes through the logger or forwards to next.

LevelLogHandler

Concrete chain node that accepts one LogLevel. defaultChain constructs the DEBUG, INFO, WARN, and ERROR sequence used by each Logger.

Logger

Application-facing facade. It stores configuration, performs level filtering, creates LogMessage objects, invokes the handler chain, formats events, and broadcasts to appenders.

LogManager

Singleton registry for loggers. The holder idiom gives lazy, thread-safe initialization, and computeIfAbsent returns one Logger per name.

Dry Run

Sample input

Create logger billing from LogManager. Set minimum level to INFO, keep the default ConsoleAppender, add a FileAppender, and use SimpleFormatter. Calls: debug for cache miss, info for payment started, and error for payment failed.

StepCallFilter decisionHandler resultFormatted outputSink effect
1getLogger(billing)Not a log eventNo handlerNo outputSingleton manager returns the cached or new Logger
2debug(cache miss)DEBUG is below INFOChain not invokedNo outputNo appender receives anything
3info(payment started)INFO passesINFO handler publishestimestamp INFO [billing] payment startedConsole and file receive the same line
4error(payment failed)ERROR passesERROR handler publishestimestamp ERROR [billing] payment failedConsole and file receive the same line
5add async appender laterNot a log eventNo handlerNo outputA new Appender can enqueue future lines without Logger changes

The important optimization is step 2: the DEBUG message is rejected before LogMessage.create, formatting, or I/O. Steps 3 and 4 show accepted events taking the same path regardless of severity.

Complexity Analysis

OperationTimeSpaceNote
getLoggerO(1) averageO(1) per distinct loggerConcurrent hash map lookup and possible insertion by name.
filtered log callO(1)O(1)A below-threshold call only compares severities and returns.
accepted log callO(H + A + W)O(1)H handlers in the chain, A appenders, and W work done by sinks such as file write length.
addAppenderO(A)O(A)CopyOnWriteArrayList copies the appender array on mutation, which is acceptable because configuration is rare.
file appendO(M)O(M)M is formatted message length converted to UTF-8 bytes.

The hot path is the accepted log call. In the reference implementation H is bounded by four levels, so practical cost is dominated by formatting and sink I/O. Async appenders shift I/O off the caller thread but add queue memory and backpressure decisions.

Extensibility

New formatter

Implement Formatter for JSON, key-value, colorized console, or redacted output, then call setFormatter on a logger.

New sink

Implement Appender for rotating files, sockets, cloud collectors, metrics streams, or tests. Logger.publish does not change.

Async logging

Add an AsyncAppender that owns a bounded queue and worker thread. It implements the same append method, so async behavior is a sink concern.

New level policy

Add a LogLevel and a matching LevelLogHandler in the chain. If levels become dynamic, replace the enum with a registry and keep the handler abstraction.

Structured context

Extend LogMessage with fields such as request id, tenant id, or key-value attributes. Existing formatters can ignore new fields while structured formatters use them.

Alternative Designs

Switch-based logger

Put level routing directly inside Logger.log with a switch over LogLevel and call appenders from each branch.

Tradeoffs

Simpler for four levels, but it removes the Chain of Responsibility seam and makes custom handling policies require logger edits.

Always-async event queue

Make Logger enqueue LogMessage objects into a shared dispatcher, and let worker threads format and write to sinks.

Tradeoffs

Better latency for callers under slow I/O, but more complex shutdown, flush, ordering, memory, and backpressure behavior.

Hierarchical logger tree

Store loggers in a package-style tree where child loggers inherit level, formatter, and appenders from parents unless overridden.

Tradeoffs

Closer to mature production frameworks, but substantially more state and precedence rules than an interview base design needs.

Common Mistakes

  • ×

    Formatting the message before checking minimumLevel, which makes filtered DEBUG logs expensive.

  • ×

    Hard-coding console and file writes inside Logger, preventing new sinks without modifying core logic.

  • ×

    Using a public constructor for LogManager and ending up with multiple logger registries.

  • ×

    Letting LogHandler.handle be overridden and accidentally breaking the forward-to-next invariant.

  • ×

    Ignoring thread safety for the appender list while one thread logs and another thread reconfigures sinks.

  • ×

    Treating async logging as one new thread per log call instead of a bounded queue and worker model.

  • ×

    Catching and swallowing file I/O failures, which makes missing logs impossible to diagnose.

Follow-up Interview Questions

QHow would you add async logging without changing application code?

Implement AsyncAppender behind the existing Appender interface. It enqueues formatted strings into a bounded queue, a worker drains to a delegate appender, and shutdown flushes remaining messages.

QHow would you support JSON logs?

Create a JsonFormatter implementing Formatter. It serializes LogMessage fields and any future structured context into one JSON line.

QHow can you prevent a slow file sink from delaying every request?

Use an async appender or a batching appender for file output. Decide a backpressure policy: block, drop DEBUG/INFO first, or fail fast when the queue is full.

QHow would you support different minimum levels for different modules?

Return one named Logger per module from LogManager and configure minimumLevel per logger. A hierarchy can be added later for inheritance.

QWhat should happen if one appender fails but another succeeds?

In production, isolate failures per appender and report them through internal diagnostics so one bad sink does not stop all logging. The reference implementation keeps failure visible for clarity.

Production Considerations

Async queue and backpressure

Use bounded queues, explicit drop or block policy, flush on shutdown, and metrics for queue depth and dropped messages.

File rotation and retention

Production file appenders need rotation by size or time, compression, retention limits, and safe rollover under concurrent writes.

Structured observability

Prefer machine-readable fields for request id, tenant, trace id, and error class. Keep text logs compatible with metrics and tracing systems.

Security and privacy

Add redaction in formatter or message creation for credentials, tokens, personal data, and payment details. Logging must not become a data leak.

Operational diagnostics

The framework itself should expose internal counters for appender failures, queue drops, bytes written, and formatting errors.

What Interviewers Look For

  • Did the candidate filter before allocation, formatting, and I/O?

  • Are handler routing, formatting, and sink delivery separated into clear abstractions?

  • Is LogManager a safe singleton rather than global mutable state with multiple instances?

  • Can the design add console, file, remote, and async sinks without editing Logger.log?

  • Did the candidate call out thread safety and backpressure instead of hand-waving async logging?

Quiz

0/5 answered

  1. 1.Why should **Logger.log** check **minimumLevel** before creating a **LogMessage**?

  2. 2.Which class represents the Singleton pattern in the reference design?

  3. 3.What is the main benefit of the **Appender** interface?

  4. 4.What does the Chain of Responsibility contribute here?

  5. 5.Where should async logging behavior fit best in this design?

Practice Variants

Build an async appender

Intermediate

Implement an AsyncAppender with a bounded queue, one worker thread, flush on shutdown, and a configurable policy for full queues.

Add JSON structured logging

Intermediate

Create JsonFormatter and extend LogMessage with optional key-value context while keeping existing appenders unchanged.

Add hierarchical logger configuration

Advanced

Support parent-child logger names such as service and service.payment, with inherited level and appenders unless overridden.

Flashcards

Cheat Sheet

Entities: LogManager, Logger, LogLevel, LogMessage, LogHandler, LevelLogHandler, Formatter, SimpleFormatter, Appender, ConsoleAppender, FileAppender.

Core flow: caller gets a named logger → logger filters by minimum level → creates LogMessage → sends through handler chain → formatter renders → appenders receive the line.

Patterns: Chain of Responsibility for level handlers, Singleton for LogManager, Strategy for Formatter and Appender, Observer for publishing to many appenders.

Invariants: filtered logs do no formatting or I/O; named loggers are reused; appenders receive identical formatted text for one event; file writes are synchronized.

Async extension: keep Logger unchanged. Add an appender with a bounded queue, worker, flush, and backpressure policy.

Complexity: filtered log O(1); accepted log O(H + A + W); getLogger O(1) average; addAppender O(A) because copy-on-write copies the appender list.

References