Problem Statement
Design a Logger Framework that supports:
-
Multiple Log Levels
- DEBUG
- INFO
- WARN
- ERROR
- FATAL
-
Multiple Output Destinations
- Console
- File
- Database
- Kafka (future)
- ElasticSearch (future)
Configurable Minimum Log Level
Thread Safe
Easy to extend
Functional Requirements
- Log message
- Timestamp
- Log Level
- Destination
- Format message
Non Functional Requirements
- Extensible
- Low Coupling
- High Cohesion
- Open Closed Principle
- Thread Safe
- Singleton Logger
Interview Expectation
Application
│
▼
Logger (Singleton)
│
▼
LogManager
│
▼
Formatter
│
▼
Appender
├── ConsoleAppender
├── FileAppender
├── DatabaseAppender
Design Patterns Used
| Pattern | Why |
|---|---|
| Singleton | One Logger instance |
| Strategy | Different Appenders |
| Factory | Create appenders |
| Builder | Build LogMessage |
| Chain of Responsibility (Optional) | Multiple appenders |
| Dependency Injection | Loose coupling |
Step 1: Log Level
public enum LogLevel {
DEBUG(1),
INFO(2),
WARN(3),
ERROR(4),
FATAL(5);
private final int priority;
LogLevel(int priority) {
this.priority = priority;
}
public int getPriority() {
return priority;
}
}
Step 2: Log Message
import java.time.LocalDateTime;
public class LogMessage {
private final LogLevel level;
private final String message;
private final LocalDateTime timestamp;
public LogMessage(LogLevel level, String message) {
this.level = level;
this.message = message;
this.timestamp = LocalDateTime.now();
}
public LogLevel getLevel() {
return level;
}
public String getMessage() {
return message;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
}
Step 3: Formatter
public interface LogFormatter {
String format(LogMessage message);
}
Default Formatter
public class DefaultFormatter implements LogFormatter {
@Override
public String format(LogMessage message) {
return String.format(
"[%s] [%s] %s",
message.getTimestamp(),
message.getLevel(),
message.getMessage());
}
}
Step 4: Appender
public interface Appender {
void append(LogMessage message);
}
Console Appender
public class ConsoleAppender implements Appender {
private final LogFormatter formatter;
public ConsoleAppender(LogFormatter formatter) {
this.formatter = formatter;
}
@Override
public void append(LogMessage message) {
System.out.println(formatter.format(message));
}
}
File Appender
import java.io.FileWriter;
import java.io.IOException;
public class FileAppender implements Appender {
private final LogFormatter formatter;
public FileAppender(LogFormatter formatter) {
this.formatter = formatter;
}
@Override
public void append(LogMessage message) {
try(FileWriter writer = new FileWriter("app.log", true)) {
writer.write(formatter.format(message));
writer.write("\n");
} catch (IOException e) {
e.printStackTrace();
}
}
}
Step 5: Logger
import java.util.List;
public class Logger {
private static volatile Logger instance;
private final List<Appender> appenders;
private LogLevel minimumLevel;
private Logger(List<Appender> appenders,
LogLevel minimumLevel) {
this.appenders = appenders;
this.minimumLevel = minimumLevel;
}
public static Logger getInstance(List<Appender> appenders,
LogLevel level) {
if (instance == null) {
synchronized (Logger.class) {
if (instance == null) {
instance = new Logger(appenders, level);
}
}
}
return instance;
}
public void log(LogLevel level,
String message) {
if(level.getPriority() < minimumLevel.getPriority())
return;
LogMessage logMessage =
new LogMessage(level, message);
for(Appender appender : appenders)
appender.append(logMessage);
}
}
Client
import java.util.List;
public class Main {
public static void main(String[] args) {
LogFormatter formatter =
new DefaultFormatter();
Appender console =
new ConsoleAppender(formatter);
Appender file =
new FileAppender(formatter);
Logger logger =
Logger.getInstance(
List.of(console, file),
LogLevel.INFO);
logger.log(LogLevel.INFO,
"Application Started");
logger.log(LogLevel.DEBUG,
"Debug Message");
logger.log(LogLevel.ERROR,
"Database Down");
}
}
Output
[2026-07-09T10:30] [INFO] Application Started
[2026-07-09T10:31] [ERROR] Database Down
(DEBUG ignored)
UML
+------------------+
| Logger |
+------------------+
| minimumLevel |
| appenders |
+------------------+
|
-------------------------------
| |
▼ ▼
+----------------+ +----------------+
| Appender | | LogFormatter |
+----------------+ +----------------+
| append() | | format() |
+----------------+ +----------------+
▲
│
-----------------------------
| | |
▼ ▼ ▼
Console File Database
Appender Appender Appender
▲
|
+--------------+
| LogMessage |
+--------------+
| level |
| timestamp |
| message |
+--------------+
SOLID Principles
| Principle | Application |
|---|---|
| SRP |
LogMessage, Appender, Formatter, Logger each have a single responsibility. |
| OCP | Add new appenders or formatters without modifying existing classes. |
| LSP | Any Appender implementation can replace another. |
| ISP | Small focused interfaces (Appender, LogFormatter). |
| DIP |
Logger depends on abstractions rather than concrete implementations. |
Common Interview Follow-up Questions
- How would you support asynchronous logging?
- Introduce a
BlockingQueue<LogMessage>and a dedicated worker thread or thread pool that consumes log events and dispatches them to appenders.
- How would you prevent logging from blocking application threads?
- Use non-blocking queues, batching, and asynchronous appenders with configurable backpressure policies.
- How would you support log rotation?
- Create a
RollingFileAppenderthat rotates files based on size or time (daily/hourly) and manages retention.
- How would you support multiple log formats?
- Implement additional
LogFormatterstrategies such asJsonFormatter,XmlFormatter, or custom templates.
- How would you enable configuration from a properties or YAML file?
- Build a
LoggerConfigurationcomponent and anAppenderFactoryto instantiate appenders based on external configuration.
- How would you make the logger thread-safe?
- Keep shared state immutable where possible, use thread-safe collections if appenders are modified at runtime, and ensure appenders handle concurrent writes correctly.
- How would you add contextual information (e.g., request ID, user ID)?
- Introduce a
Mapped Diagnostic Context (MDC)usingThreadLocalto automatically enrich eachLogMessage.
- How would you support filtering?
- Add a
LogFilterabstraction (Strategy pattern) to filter messages by package, marker, or custom predicates before dispatching.
Possible Extensions
AsyncLoggerRollingFileAppenderDatabaseAppenderKafkaAppenderElasticSearchAppenderJsonFormatterXMLFormatterLoggerFactoryLoggerConfigurationLogFilter-
MDC(Mapped Diagnostic Context) - Rate limiting and sampling
- Dynamic log level updates
- Metrics for dropped/processed log events
This design demonstrates strong OOP fundamentals, SOLID principles, and extensibility while remaining concise enough to explain in a typical 30–45 minute LLD interview.
Top comments (0)