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
Enter fullscreen mode Exit fullscreen mode
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 couplingStep 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;
}
}
Enter fullscreen mode Exit fullscreen mode
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;
}
}
Enter fullscreen mode Exit fullscreen mode
Step 3: Formatter
public interface LogFormatter {
String format(LogMessage message);
}
Enter fullscreen mode Exit fullscreen mode
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());
}
}
Enter fullscreen mode Exit fullscreen mode
Step 4: Appender
public interface Appender {
void append(LogMessage message);
}
Enter fullscreen mode Exit fullscreen mode
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));
}
}
Enter fullscreen mode Exit fullscreen mode
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();
}
}
}
Enter fullscreen mode Exit fullscreen mode
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);
}
}
Enter fullscreen mode Exit fullscreen mode
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");
}
}
Enter fullscreen mode Exit fullscreen mode
Output
[2026-07-09T10:30] [INFO] Application Started
[2026-07-09T10:31] [ERROR] Database Down
Enter fullscreen mode Exit fullscreen mode
(DEBUG ignored)
UML
+------------------+
| Logger |
+------------------+
| minimumLevel |
| appenders |
+------------------+
|
-------------------------------
| |
▼ ▼
+----------------+ +----------------+
| Appender | | LogFormatter |
+----------------+ +----------------+
| append() | | format() |
+----------------+ +----------------+
▲
│
-----------------------------
| | |
▼ ▼ ▼
Console File Database
Appender Appender Appender
▲
|
+--------------+
| LogMessage |
+--------------+
| level |
| timestamp |
| message |
+--------------+
Enter fullscreen mode Exit fullscreen mode
SOLID Principles
Principle Application SRPLogMessage, 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.
답글 남기기