Well defined logging library API with get and set methods and design pattern used
Well-defined in-memory logging library API in Java with get and set methods. This example uses a thread-safe design to support concurrent access and avoids persistent storage (i.e., in-memory only).
✅ Features:
In-memory log storage
Thread-safe
setLog(level, message)
to add logsgetLogs()
to retrieve all logs or filter by log levelClear API interface for easy integration
Here's a theoretical explanation of the in-memory logging library design and the reasoning behind the choices made:
✅ Objective:
To design a lightweight logging library that:
Stores logs in memory (not persisted to disk or external storage)
Supports basic operations to add (
setLog
) and retrieve (getLogs
) logsIs thread-safe for use in concurrent applications
Can filter logs based on log levels (INFO, ERROR, etc.)
🔍 Design Components Explained
1. LogLevel
Enum
Purpose: Defines the severity/type of logs.
Values:
DEBUG
,INFO
,WARN
,ERROR
Reason: Using an enum restricts log levels to a controlled set of constants, improving code safety and readability.
2. LogEntry
Class
Purpose: Acts as a data container for individual log messages.
Attributes:
timestamp
: The time the log was createdlevel
: The severity (e.g., INFO, ERROR)message
: The actual log message
Why immutable:
All fields are
final
, initialized only in the constructorPromotes thread safety and predictability
3. InMemoryLogger
Class
Core class that:
Accepts new logs via
setLog(level, message)
Retrieves logs using
getLogs()
(with or without filtering)Provides utility method
clearLogs()
to reset state
🧠 Data Structure:
Uses
Collections.synchronizedList(new ArrayList<>())
Ensures thread-safe operations (multiple threads can log concurrently)
ArrayList
gives fast append performanceSynchronization ensures consistent behavior in multi-threaded apps
| Method | Purpose |
| -------------------------- | ------------------------------ |
| `setLog(LogLevel, String)` | Adds a new log entry |
| `getLogs()` | Returns a copy of all logs |
| `getLogs(LogLevel)` | Returns logs filtered by level |
| `clearLogs()` | Clears all log entries |
🧵 Thread Safety Considerations
Synchronized list ensures concurrent
add
/read
do not cause race conditions.Explicit
synchronized
blocks during read operations (getLogs
,getLogs(level)
) avoid ConcurrentModificationException.
| Aspect | Comments |
| --------------- | ------------------------------------------------------------------------------------------------------------- |
| Memory | Logs are stored in memory – so **memory usage grows** with time unless `clearLogs()` or a size limit is used. |
| No persistence | Logs are **lost if the application shuts down** – good for lightweight or test use cases. |
| No async writes | Simpler but **not optimized for high-throughput logging**. Could be extended with queues. |
🔧 Possible Enhancements
TTL for log entries (e.g., auto-remove logs older than X minutes)
Bounded log size (to cap memory usage)
JSON or structured output formatting
Support for tags or context (e.g., requestId, userId)
Integration with external logging systems for persistence (e.g., Logstash, Elasticsearch)
✅ Use Cases
Testing or debugging libraries
Microservices needing lightweight logging
Embedded systems or internal tools
Temporary log collection for analytics or audits
Design pattern to be used
✅ 1. Singleton Pattern
📌 Why:
You want a single instance of the logger throughout your application (just like Log4j
, SLF4J
, or java.util.logging.Logger
), so logs are written to the same in-memory store.
💡 How:
public class InMemoryLogger {
private static final InMemoryLogger instance = new InMemoryLogger();
private InMemoryLogger() {} // private constructor
public static InMemoryLogger getInstance() {
return instance;
}
// ... logging methods ...
}
✅ 2. Factory Pattern (optional)
📌 Why:
If you want to create log entries or formatters in a decoupled way, a LogEntryFactory
or LogFormatterFactory
can help.
💡 Example:
public class LogEntryFactory {
public static LogEntry create(LogLevel level, String message) {
return new LogEntry(level, message);
}
}
✅ 3. Strategy Pattern (for extensibility)
📌 Why:
To allow pluggable behaviors, such as:
Different formatting strategies
Different output sinks (console, memory, file)
💡 Example:
public interface LogOutputStrategy {
void log(LogEntry entry);
}
public class InMemoryLogOutput implements LogOutputStrategy {
@Override
public void log(LogEntry entry) {
// store in memory
}
}
public class ConsoleLogOutput implements LogOutputStrategy {
@Override
public void log(LogEntry entry) {
System.out.println(entry);
}
}
Then in the logger:
private LogOutputStrategy outputStrategy = new InMemoryLogOutput(); // default
public void setLog(LogLevel level, String message) {
LogEntry entry = new LogEntry(level, message);
outputStrategy.log(entry);
}
✅ 4. Builder Pattern (for LogEntry or configuration)
📌 Why:
To allow flexible, readable, and extensible construction of log entries or logger config (e.g., with timestamp, thread info, etc.)
💡 Example:
public class LogEntryBuilder {
private LogLevel level;
private String message;
public LogEntryBuilder withLevel(LogLevel level) {
this.level = level;
return this;
}
public LogEntryBuilder withMessage(String message) {
this.message = message;
return this;
}
public LogEntry build() {
return new LogEntry(level, message);
}
}
| Pattern | Use in Logger |
| --------- | -------------------------------------- |
| Singleton | Ensure only one logger instance exists |
| Factory | Decouple creation of log entries |
| Strategy | Swap log storage/output strategies |
| Builder | Clean construction of logs/config |
Extended Code (extra functionality like auto rotate,notify logs and timestamp).
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class InMemoryLogger {
private static volatile InMemoryLogger instance;
private static final int MAX_LOGS = 1000; // auto-rotate threshold
private final List<LogEntry> logs = Collections.synchronizedList(new ArrayList<>());
private InMemoryLogger() {}
public static InMemoryLogger getInstance() {
if (instance == null) {
synchronized (InMemoryLogger.class) {
if (instance == null) {
instance = new InMemoryLogger();
}
}
}
return instance;
}
public void setLogs(LogLevel logLevel, String message) {
synchronized (logs) {
if (logs.size() >= MAX_LOGS) {
logs.clear(); // Auto-rotate logs
logs.add(new LogEntry(LogLevel.WARN, "Log rotated due to size > " + MAX_LOGS));
}
LogEntry entry = new LogEntry(logLevel, message);
logs.add(entry);
if (logLevel == LogLevel.ERROR) {
notifyError(entry); // Notify on error
}
}
}
public List<LogEntry> getLogs() {
synchronized (logs) {
return new ArrayList<>(logs);
}
}
public List<LogEntry> getLogs(LogLevel logLevel) {
synchronized (logs) {
return logs.stream()
.filter(log -> log.getLogLevel() == logLevel)
.collect(Collectors.toList());
}
}
public void clearLogs() {
synchronized (logs) {
logs.clear();
}
}
private void notifyError(LogEntry entry) {
// Print to stderr; in real app, this could send an email, push, etc.
System.err.println("!!! ERROR LOGGED: " + entry.toString());
}
// Inner class for LogEntry with timestamp
public static class LogEntry {
private final LogLevel logLevel;
private final String message;
private final LocalDateTime timestamp;
public LogEntry(LogLevel logLevel, String message) {
this.logLevel = logLevel;
this.message = message;
this.timestamp = LocalDateTime.now();
}
public LogLevel getLogLevel() {
return logLevel;
}
public String getMessage() {
return message;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
@Override
public String toString() {
return "[" + timestamp + "][" + logLevel + "] " + message;
}
}
}