Design a Pluggable Logger Framework
In a 45-minute LLD round, the interviewer wants to see clean decoupling between log producers, severity filtering, and I/O destinations. Scope the solution to synchronous in-memory routing with DEBUG < INFO < WARN < ERROR severity ordering, a pipeline of handlers, and pluggable sinks. Mention async lock-free ring buffers as a follow-up extension.
Problem statement & clarifying requirements
Design an extensible, enterprise-grade in-process logging framework. The system must support multiple severity levels (DEBUG, INFO, WARN, ERROR), route log messages through a configurable pipeline of level filters, and dispatch messages to pluggable output destinations (such as stdout console, file, or an in-memory ring buffer) without coupling log producers to specific destination implementations.
Support four standard levels: DEBUG < INFO < WARN < ERROR. A filter set to severity level L will process all messages with severity >= L.
Treat destinations as interchangeable strategies (LogSink). Each handler in the filter chain can hold zero or more sinks and broadcast messages to them.
Provide a globally accessible Logger singleton facade offering ergonomic helper methods (debug(), info(), warn(), error()) while keeping the underlying filter pipeline configurable at startup.
Focus on the synchronous object-oriented architecture (Chain of Responsibility + Strategy + Singleton). Leave lock-free ring-buffer thread offloading as an explicit architectural extension.
First, sketch the design yourself
Spend 5 minutes listing the classes, who owns whom, and which interface hides the part that changes.
- 1.
Never hardcode
std::coutorprintfinside handler classes. Decouple message filtering from I/O emission using the Strategy pattern (LogSink). - 2.
Avoid a giant
switch (level)statement inside the logger. Use the Chain of Responsibility pattern to decouple level-specific routing and allow dynamic reordering or filtering. - 3.
Pay close attention to ownership: the
Loggerfacade owns the rootLogHandleruniquely viastd::unique_ptr, whileLogSinkinstances are shared across handlers usingstd::shared_ptr.
How to think through this problem (5-step walkthrough)
In a 45-minute LLD round, the interviewer wants to see clean decoupling between log producers, severity filtering, and I/O destinations. Scope the solution to synchronous in-memory routing with DEBUG < INFO < WARN < ERROR severity ordering, a pipeline of handlers, and pluggable sinks. Mention async lock-free ring buffers as a follow-up extension.
Not every noun in the prompt deserves its own class. Separate dumb data structs from stateful managers, and turn trivial nouns into fields or enums.
LogLevelOrdered enum class LogLevel { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3 } so severity comparisons (level >= minLevel_) are a single integer check.
LogSinkOutput destinations (Console, File, MemoryBuffer, HTTP) vary independently from routing rules β classic Strategy interface.
LogHandlerEach node in the Chain of Responsibility holds a minimum severity threshold, a list of attached sinks, and a unique_ptr<LogHandler> to the next handler.
LoggerThread-safe Meyers Singleton entry point (Logger::getInstance()) that owns the head of the handler chain.
FormattedString / LogLineTrap noun β don't create a heavy class hierarchy for formatted text; a helper function or std::format call inside the sink is simpler and faster.
A beginner writes a single Logger::log(string level, string msg) method with if (level == "ERROR") { writeToFile(msg); writeToConsole(msg); } else if (level == "INFO") .... Adding a new sink (like Datadog or an in-memory test buffer) or changing which levels go to which files forces editing the core Logger class and breaks the Open-Closed Principle.
Hardcoding std::cout or std::ofstream inside Logger makes unit testing impossible without filesystem side effects.
A rigid switch(level) couples level filtering to specific sinks.
Passing a Logger& into every constructor in the codebase pollutes business signatures.
- β
Ownership:
Loggerexclusively owns the headLogHandlerviastd::unique_ptr<LogHandler>, and each handler owns itsnext_link viastd::unique_ptr<LogHandler>. - β
Shared sinks: Because both the
INFOhandler andERRORhandler may write to the same console or file sink, handlers holdstd::shared_ptr<LogSink>. - β
Chain invariant: Calling
handle(level, msg)always forwards tonext_->handle(level, msg)even when the current node also matches, so anERRORmessage can be recorded in both the general log and the error-only alert sink.
Entities & design patterns used
Unlock Full UML Diagram & C++23 Solution
The complete UML diagram, production-ready C++23 code, follow-up extensions, and design pitfall analysis are part of Brute2Best Pro. All 7 foundation chapters are 100% free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.