Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • About
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

👥10,102visitors

Built for engineers who want to go deep.


← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
Chaturmind
← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
HomeLearnSystem DesignLow-Level Design & Design PatternsMachine Coding Exercises
✓ FreeIntermediate· 8 min read

Design a Logging Framework

Logger/LogLevel/Appender/Formatter classes, Chain of Responsibility for level filtering across multiple appenders, and thread safety for concurrent writes.

Published September 23, 2026


Design a Logging Framework

Core classes

enum LogLevel { DEBUG(0), INFO(1), WARN(2), ERROR(3); final int severity; LogLevel(int s) { severity = s; } }

interface Appender { void write(LogLevel level, String message); LogLevel getMinLevel(); }
interface Formatter { String format(LogLevel level, String message, Instant timestamp); }

class ConsoleAppender implements Appender {
    private final LogLevel minLevel;
    private final Formatter formatter;
    public void write(LogLevel level, String message) { System.out.println(formatter.format(level, message, Instant.now())); }
    public LogLevel getMinLevel() { return minLevel; }
}
class FileAppender implements Appender { /* similar, writes to a file */ }
class NetworkAppender implements Appender { /* similar, ships to a log aggregator */ }

Advertisement

Level filtering via Chain of Responsibility

class Logger {
    private final List<Appender> appenders;

    void log(LogLevel level, String message) {
        for (Appender appender : appenders) {
            if (level.severity >= appender.getMinLevel().severity) { // each appender independently decides to handle or pass
                appender.write(level, message);
            }
        }
    }
}

Each Appender independently decides whether a given log call meets its own minimum severity — a ConsoleAppender configured for INFO and a FileAppender configured for ERROR can both be registered on the same Logger, each filtering independently. This is Chain of Responsibility's core idea (see Chain of Responsibility) applied without an explicit "pass to next" call — every appender in the list gets a chance to handle the same log event, rather than one handler claiming it exclusively, which fits logging's actual requirement (multiple destinations, not a single winner).

Thread safety for concurrent writes

class FileAppender implements Appender {
    private final BufferedWriter writer; // NOT thread-safe by default

    public synchronized void write(LogLevel level, String message) { // synchronize the actual I/O
        try { writer.write(formatter.format(level, message, Instant.now())); writer.newLine(); }
        catch (IOException e) { /* handle */ }
    }
}

Multiple application threads logging concurrently to the same file (or console) need the actual write operation synchronized — without it, two threads' output can interleave mid-line, producing garbled, unreadable log output (not a crash, just corrupted data). A synchronized method on the appender is the simplest correct fix; a production logging framework typically also buffers writes and flushes asynchronously on a dedicated thread, to avoid every application thread blocking on I/O directly for every single log call.

Asynchronous appending with bounded memory

Writing to a file or network on the calling thread adds I/O latency to every request that logs. The standard fix is an async appender: the calling thread puts the event into a queue, and a background thread writes it out. The important design decisions are what happens when the queue fills up, and how to shut down without losing logs.

record LogEvent(Instant timestamp, LogLevel level, String loggerName, String thread,
                String message, Throwable error, Map<String, String> context) {}

class AsyncAppender implements Appender, AutoCloseable {
    private final Appender delegate;
    private final BlockingQueue<LogEvent> queue;
    private final Thread worker;
    private final AtomicLong dropped = new AtomicLong();
    private volatile boolean running = true;

    AsyncAppender(Appender delegate, int capacity) {
        this.delegate = delegate;
        this.queue = new ArrayBlockingQueue<>(capacity);       // BOUNDED: never grow without limit
        this.worker = new Thread(this::drain, "async-log-writer");
        this.worker.setDaemon(true);
        this.worker.start();
    }

    @Override public void append(LogEvent e) {
        if (e.level().severity >= LogLevel.WARN.severity) {
            try { queue.put(e); }                               // never drop warnings or errors: wait briefly
            catch (InterruptedException ie) { Thread.currentThread().interrupt(); }
        } else if (!queue.offer(e)) {
            dropped.incrementAndGet();                          // shed DEBUG/INFO under pressure
        }
    }

    private void drain() {
        List<LogEvent> batch = new ArrayList<>(256);
        while (running || !queue.isEmpty()) {
            try {
                LogEvent first = queue.poll(200, TimeUnit.MILLISECONDS);
                if (first == null) continue;
                batch.add(first);
                queue.drainTo(batch, 255);                      // write in batches: far fewer syscalls
                batch.forEach(delegate::append);
                delegate.flush();
            } catch (InterruptedException ie) {
                running = false;
            } catch (Exception writeFailure) {
                System.err.println("log write failed: " + writeFailure);   // never throw back into the app
            } finally {
                batch.clear();
            }
            long lost = dropped.getAndSet(0);
            if (lost > 0) delegate.append(droppedNotice(lost));   // make the loss visible
        }
    }

    @Override public void close() throws InterruptedException {
        running = false;
        worker.join(5_000);                                     // flush what's queued at shutdown
        delegate.flush();
    }
}

The trade-offs to state explicitly:

  • Bounded queue: an unbounded queue turns a slow disk into an OutOfMemoryError.
  • An overflow policy by level: dropping DEBUG and INFO under pressure keeps the application fast, while blocking briefly for WARN and ERROR preserves the events you need most during an incident. Logback's AsyncAppender makes a similar choice with discardingThreshold.
  • Counting and reporting drops, so that missing logs aren't silently missing.
  • Batching: one flush per batch instead of per line gives a large throughput gain.
  • Logging must never throw into application code. A full disk shouldn't fail a payment.
  • A shutdown hook (or close() via the framework) flushes the queue, otherwise the last seconds of logs, often the most important ones, are lost on exit.

Contextual data (MDC) and structured output

Request IDs and user IDs should appear on every log line of a request without being passed to every method. A Mapped Diagnostic Context stores them per thread:

final class LogContext {
    private static final ThreadLocal<Map<String, String>> CTX = ThreadLocal.withInitial(HashMap::new);
    static void put(String k, String v) { CTX.get().put(k, v); }
    static Map<String, String> snapshot() { return Map.copyOf(CTX.get()); }   // copied into each LogEvent
    static void clear() { CTX.remove(); }                                      // essential on pooled threads
}

Logger copies LogContext.snapshot() into each LogEvent at the moment of logging, because the async writer runs on another thread, where the ThreadLocal is empty. A web filter sets requestId at the start of each request, and clears it in finally. Otherwise, a pooled thread leaks one request's ID into the next request's logs.

Emit logs as structured JSON ({"ts":…,"level":"ERROR","requestId":…,"msg":…}) rather than free text, so the log platform can filter by field instead of using regular expressions. A JsonFormatter implementing the existing Formatter interface makes this a configuration choice.

Avoiding wasted work

log.debug("Order state: " + order.toDetailedString());   // string built even when DEBUG is off
log.debug("Order state: {}", order);                     // formatted only if DEBUG is enabled
if (log.isDebugEnabled()) log.debug("Dump: {}", expensiveDump());   // guard genuinely expensive arguments

Check the level before formatting. Parameterized messages and level guards are why real logging APIs look the way they do.

Follow-up questions this topic invites — and their answers

Q: Why does each Appender hold its own minLevel rather than the Logger filtering once globally? A: Different destinations often need different verbosity — you might want DEBUG-level detail in a local file for troubleshooting but only ERROR-level alerts sent over the network, which requires per-destination filtering, not one global level applied uniformly everywhere.

Q: How would you avoid the console/file write blocking the calling thread on every log call? A: Route log messages through an internal queue (a BlockingQueue, see Concurrent Utilities & Coordination) that a dedicated background thread drains and writes — the calling application thread just enqueues and returns immediately, decoupling application throughput from I/O latency, at the cost of a small risk of losing buffered-but-unflushed messages on a crash.

Q: Is synchronized the right choice long-term, or would a lock-free approach be better under high log volume? A: For genuinely high-throughput logging, a lock-free ring buffer (the approach libraries like Log4j2's async appender use) avoids the contention a single synchronized method creates under many concurrent logging threads — worth naming as the production-grade evolution of this design, while synchronized remains a correct, simple starting point.

Q: How does this design relate to Distributed Tracing's correlation ID propagation? A: A production Logger implementation typically includes the current correlation/trace ID (from thread-local or reactive context, see Distributed Tracing) automatically in every formatted log line — extending this design's Formatter to pull that context in is the natural connection point between structured logging and distributed tracing.

Q: What should an async logger do when its queue is full? A: Choose deliberately. Dropping low-severity events keeps the application fast, and blocking protects important events at the cost of latency. A common compromise: drop DEBUG and INFO, block briefly for WARN and ERROR, and count drops so they're visible.

Q: Why must MDC context be cleared at the end of each request? A: Server threads are pooled and reused. If the context isn't removed, the next request handled by that thread inherits the previous request's IDs, producing misleading logs and possibly leaking user data into the wrong entries.

Q: Why prefer log.debug("x={}", x) over string concatenation? A: Concatenation builds the string (and calls toString()) even when DEBUG is disabled. Parameterized messages defer formatting until the level check passes, which saves CPU and garbage-collection pressure on hot paths.

Advertisement

Previous

Splitwise / Expense Sharing System

Next

Design a Notification/Observer-Based Pub-Sub

AI Tutor

Lesson: Design a Logging Framework

Quick actions

AI responses can be inaccurate. Verify critical information.