Logging framework
Low-level design of a pluggable logging framework -- log levels, appender pipeline, formatter strategy, logger hierarchy, async buffered writes, and structured JSON output.
The Problem
Every production service needs logging, and most teams learn this the hard way. Your team started with System.out.println sprinkled across the codebase. Now 40 microservices print unstructured text to stdout. The ops team cannot search for a specific request across services, nobody can filter by severity, and synchronous file writes add 50ms per request under load.
A pluggable logging framework routes records through a pipeline: check severity, run filters, format the message, and dispatch to appenders (console, file, rotating file, HTTP). Each stage is swappable. Loggers form a hierarchy so com.app.service.UserService inherits configuration from com.app unless overridden. An async appender wraps any destination to decouple the calling thread from I/O.
Design the core classes for a logging framework that supports configurable log levels, a logger hierarchy with inheritance, pluggable formatters, composable appenders, filter chains, and async buffered output.
Requirements
Clarifying Questions
Before jumping into class design, ask questions to turn the vague prompt into a concrete specification. Cover four areas: core actions, error handling, boundaries, and future extensions.
You: "What log levels should the framework support, and how does the level hierarchy work?"
Interviewer: "Six levels in ascending severity: TRACE, DEBUG, INFO, WARN, ERROR, FATAL. A logger configured at INFO discards TRACE and DEBUG messages but passes INFO, WARN, ERROR, and FATAL."
Six levels with ordinal comparison. An enum with ordinal() provides fast level checks on the hot path.
You: "Can a single logger write to multiple appenders? For example, console and file at the same time?"
Interviewer: "Yes. Each logger has a list of appenders. A log record is dispatched to every appender in the list."
Multiple appenders per logger. The dispatch loop iterates an appender list, which needs to be thread-safe.
You: "Should loggers form a hierarchy? If I configure com.app at WARN, does com.app.service.UserService also log at WARN?"
Interviewer: "Yes. Child loggers inherit the parent's level and appenders unless explicitly overridden. This is how Log4j and Logback work."
Logger hierarchy. The registry resolves parents by walking the dotted name backwards: com.app.service checks com.app, then com, then ROOT.
You: "Do we need async logging so the calling thread is not blocked by file I/O?"
Interviewer: "Yes. An async appender wraps any other appender, buffers records in a bounded queue, and a background thread drains the queue. If the queue fills up, drop the record or block, depending on configuration."
Async appender with overflow policy. A decorator around any Appender with a bounded queue and background drain thread.
You: "Should log messages support structured key-value context, like a request ID or user ID attached to every log line?"
Interviewer: "Yes. A Mapped Diagnostic Context (MDC) stores per-thread key-value pairs. The formatter includes them in the output automatically."
MDC using ThreadLocal. The formatter reads from it when rendering.
You: "Should we support multiple output formats? Plain text, JSON, and pattern-based like {timestamp} [{level}] {message}?"
Interviewer: "Yes. The formatter is a pluggable strategy. Ship with plain text, JSON, and a configurable pattern formatter."
Three built-in formatters, with a Formatter strategy interface for custom ones.
You: "Do we need log rotation? Size-based or time-based?"
Interviewer: "Support size-based rotation: when the file exceeds a configured max size, close it, rename to a numbered backup, and open a fresh file. Time-based is an extension."
Size-based rotation in the file appender.
Perfect. You have now clarified scope and ruled out unnecessary complexity.
Final Requirements
Functional Requirements:
getLogger(name)returns a logger from a singleton registry, creating it if needed- Six log levels (TRACE through FATAL) with ordinal filtering: messages below the configured level are discarded
- Logger hierarchy: child loggers inherit parent level and appenders unless overridden
- Pluggable formatters (plain text, JSON, configurable pattern) via a Strategy interface
- Composable appenders: console, file, rotating file, and async wrapper
- Filter chain: a list of filters evaluated before dispatch (level filter, package filter, rate-limit filter)
- Async appender with bounded queue, background drain thread, and configurable overflow policy (DROP or BLOCK)
- MDC (Mapped Diagnostic Context) for per-thread key-value context
Non-Functional Requirements:
- Thread-safe: concurrent logging from hundreds of threads
- Fast level check on the hot path (nanoseconds, not microseconds)
- Extensible: adding a new formatter or appender requires one class, no changes to existing code
Out of Scope: Config file parsing, time-based rotation, HTTP appender, log aggregation, persistence.
Interview tip
Numbering your requirements makes it easy to reference them later: "This class satisfies requirements 4 and 5." Traceability keeps the design discussion focused.
Example Inputs and Outputs
Scenario 1: Basic level filtering
- Input: Logger
com.app.service.OrderServiceconfigured at INFO. Calllogger.debug("cache hit") - Expected: Message is discarded. The DEBUG ordinal is below INFO. No appender is invoked.
- Why: Validates requirement 2 (level filtering on the hot path).
Scenario 2: Structured JSON output with MDC
- Input: MDC contains
{"requestId": "abc-123"}. Calllogger.info("Order placed")with a JSON formatter attached. - Expected output:
{"timestamp":"2026-04-04T10:15:30Z","level":"INFO","logger":"com.app.OrderService","message":"Order placed","requestId":"abc-123"}
- Why: Validates requirements 4 (JSON formatter) and 8 (MDC context).
Scenario 3: Async non-blocking write
- Input: Logger has an async appender wrapping a file appender. Queue capacity is 1000.
- Expected: The calling thread enqueues and returns immediately. Background thread drains, formats, writes. If the queue is full and policy is DROP, the record is discarded.
- Why: Validates requirement 7 (async buffered output).
Try It Yourself
Try it yourself
Before reading the solution, spend 15-20 minutes sketching the core entities and their relationships. Focus on which parts change independently: output format, output destination, and filtering logic are three separate axes of variation. If you can identify those, the design patterns fall out naturally. Compare your approach with the walkthrough below.
30-Second Design Summary
The logging pipeline is intentionally split into independent stages: Logger performs the level check and builds a LogRecord; filters decide whether it should proceed; a Formatter turns structured data into text; and one or more Appenders write it. LoggerRegistry resolves names and hierarchy, LoggerConfig supplies inherited policies, and MDC adds thread-local context. The asynchronous appender is a bounded handoff with an explicit DROP or BLOCK overflow policy.
5-Minute Walkthrough
- Bound the scope. The design covers logger lookup, levels, hierarchy, filters, formatters, appenders, MDC, and async dispatch. Config parsing, rotation, remote aggregation, and persistence are extension topics.
- Keep the record structured. A log call captures timestamp, level, logger name, message, context, and throwable in an immutable
LogRecordbefore formatting. This allows every output target to choose its own representation. - Run the hot path. Check the effective level first, evaluate the filter chain, resolve inherited appenders, and format only accepted records. Cheap rejection protects application latency.
- Dispatch safely. Synchronous appenders write directly; the async wrapper enqueues into a bounded queue and a drain thread calls the delegate. Overflow behavior is part of the contract, not an accidental queue detail.
- Explain lifecycle. Logger configuration can be inherited or overridden by name, MDC must be cleared at request boundaries, and appender failures should be isolated so one destination cannot take down application code.
Step 1: Identify Core Entities
Start by asking: what are the main "things" in this problem? Scan the requirements for nouns and responsibilities. A logging framework has three independent axes of variation: what gets logged (levels, filters), how it is formatted (text, JSON, pattern), and where it goes (console, file, network). Each axis maps to a separate abstraction.
A common mistake is dumping everything into a single Logger class. That violates SRP because every new format or destination forces edits to the same monolith. Keep each concern in its own class.
| Entity | Responsibility | Key attributes |
|---|---|---|
| LogLevel | Enum with six severity levels and ordinal comparison | TRACE..FATAL |
| LogRecord | Immutable snapshot of one log event | timestamp, level, loggerName, message, context, throwable |
| Logger | Accepts log calls, checks level, runs filters, dispatches to appenders | name, config, parent |
| LoggerConfig | Level, appenders, filters, formatter, additivity flag for one logger | level, appenders, filters, formatter, additivity |
| LoggerRegistry | Singleton that creates, caches, and resolves loggers in the hierarchy | name-to-logger map, root |
| Formatter | Strategy: converts LogRecord to string | format(LogRecord): String |
| Appender | Strategy: writes formatted output to a destination | append(LogRecord, Formatter): void |
| Filter | CoR link: returns ACCEPT, DENY, or NEUTRAL | decide(LogRecord): FilterDecision |
| MDC | Thread-local key-value context for request tracing | put, snapshot, clear |
Notice Formatter and Appender are separate interfaces. A JSON formatter works with any appender. A file appender works with any formatter. M x N combinations from M formatters and N appenders, not M*N classes.
Step 2: Define Relationships and Class Design
Class Diagram
Class Interface Derivation
LoggerRegistry
The entry point. LoggerRegistry.getLogger("com.app.service.OrderService") returns a fully configured logger. Singleton because the application shares one hierarchy.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.