Singleton Pattern
Logger Design
Software Best Practices
Software Architecture
Design Patterns

Is it a good practice to have logger as a singleton?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Using a singleton for logging is widely considered acceptable — and most major logging frameworks already implement this pattern internally. Python's logging.getLogger(), Java's LoggerFactory.getLogger(), and .NET's ILoggerFactory all return shared instances keyed by name. The singleton pattern works well for loggers because loggers are stateless services (they format and route messages), they need global accessibility, and creating multiple instances for the same log target wastes resources. However, dependency injection of logger interfaces is preferred in modern architectures for testability.

Why Singleton Works for Loggers

java
1// Java: SLF4J already uses singleton pattern internally
2import org.slf4j.Logger;
3import org.slf4j.LoggerFactory;
4
5public class OrderService {
6    // getLogger returns the same instance for the same name
7    private static final Logger log = LoggerFactory.getLogger(OrderService.class);
8
9    public void processOrder(String orderId) {
10        log.info("Processing order: {}", orderId);
11    }
12}
python
1# Python: getLogger is a singleton registry
2import logging
3
4# Both return the SAME logger instance
5logger1 = logging.getLogger("myapp")
6logger2 = logging.getLogger("myapp")
7assert logger1 is logger2  # True — same object
8
9logger1.setLevel(logging.DEBUG)
10print(logger2.level)  # DEBUG — shared state

Logging frameworks use a registry pattern (a form of singleton): the first call creates the logger, subsequent calls return the cached instance. This ensures all code using the same logger name shares configuration (handlers, level, formatters).

Singleton Logger Implementation

python
1# Custom singleton logger (educational — use logging.getLogger in practice)
2import logging
3import threading
4
5class AppLogger:
6    _instance = None
7    _lock = threading.Lock()
8
9    def __new__(cls):
10        if cls._instance is None:
11            with cls._lock:
12                if cls._instance is None:
13                    cls._instance = super().__new__(cls)
14                    cls._instance._setup()
15        return cls._instance
16
17    def _setup(self):
18        self.logger = logging.getLogger("app")
19        self.logger.setLevel(logging.DEBUG)
20        handler = logging.StreamHandler()
21        handler.setFormatter(logging.Formatter(
22            "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
23        ))
24        self.logger.addHandler(handler)
25
26    def info(self, msg, *args):
27        self.logger.info(msg, *args)
28
29    def error(self, msg, *args):
30        self.logger.error(msg, *args)
31
32# Usage — same instance everywhere
33log = AppLogger()
34log.info("Application started")
java
1// Java singleton logger (educational — use SLF4J in practice)
2public class AppLogger {
3    private static volatile AppLogger instance;
4    private final Logger logger;
5
6    private AppLogger() {
7        logger = LoggerFactory.getLogger(AppLogger.class);
8    }
9
10    public static AppLogger getInstance() {
11        if (instance == null) {
12            synchronized (AppLogger.class) {
13                if (instance == null) {
14                    instance = new AppLogger();
15                }
16            }
17        }
18        return instance;
19    }
20
21    public void info(String msg, Object... args) {
22        logger.info(msg, args);
23    }
24}
java
1// Modern approach: inject the logger interface
2public class OrderService {
3    private final Logger logger;
4
5    // Logger injected via constructor
6    public OrderService(Logger logger) {
7        this.logger = logger;
8    }
9
10    public void processOrder(String orderId) {
11        logger.info("Processing order: {}", orderId);
12    }
13}
14
15// In Spring Boot, use Lombok for automatic injection
16@Slf4j  // Lombok creates: private static final Logger log = ...
17@Service
18public class OrderService {
19    public void processOrder(String orderId) {
20        log.info("Processing order: {}", orderId);
21    }
22}
csharp
1// C# / .NET: ILogger is injected by DI container
2public class OrderService
3{
4    private readonly ILogger<OrderService> _logger;
5
6    public OrderService(ILogger<OrderService> logger)
7    {
8        _logger = logger;
9    }
10
11    public void ProcessOrder(string orderId)
12    {
13        _logger.LogInformation("Processing order: {OrderId}", orderId);
14    }
15}

Dependency injection decouples the class from the specific logging implementation. In tests, you can inject a mock logger to verify log output without touching the console or files.

Testing with Injected Loggers

python
1# Testing with a mock logger
2from unittest.mock import MagicMock
3
4def test_order_processing():
5    mock_logger = MagicMock()
6    service = OrderService(logger=mock_logger)
7
8    service.process_order("ORD-123")
9
10    mock_logger.info.assert_called_once_with("Processing order: %s", "ORD-123")
java
1// Java: Mockito test with injected logger
2@Test
3void testOrderProcessing() {
4    Logger mockLogger = mock(Logger.class);
5    OrderService service = new OrderService(mockLogger);
6
7    service.processOrder("ORD-123");
8
9    verify(mockLogger).info("Processing order: {}", "ORD-123");
10}

With a singleton logger, verifying log output in tests requires intercepting the logger's handlers or using test-specific appenders — which is more complex than injecting a mock.

When Singleton Logger Is Fine

 
1Acceptable:
2- Small scripts and CLI tools
3- Prototypes and throwaway code
4- When the logging framework already provides singleton behavior (getLogger)
5- When you don't need to verify log output in unit tests
6
7Avoid:
8- Library code distributed to other developers (couple their code to your logger)
9- Microservices with complex DI containers (use the container's logger)
10- When log output verification is part of your test suite
11- When you need different log configurations per component instance

Common Pitfalls

  • Coupling code to a specific logging implementation: A singleton AppLogger that directly uses java.util.logging or print() makes it impossible to switch logging backends. Use a logging facade (SLF4J, Python's logging) or inject an interface.
  • Thread safety in custom singletons: Without proper synchronization (volatile + double-checked locking in Java, threading.Lock in Python), two threads can create separate instances. Use the framework's built-in singleton (getLogger) instead of writing your own.
  • Difficult to test: Singleton loggers make it hard to verify log output in unit tests. You must either capture handler output or use reflection to replace the singleton. Dependency injection solves this cleanly.
  • Hidden dependency: Classes that call AppLogger.getInstance() internally have an invisible dependency. Constructor injection makes dependencies explicit, improving readability and refactoring safety.
  • Configuration ordering issues: Singleton loggers initialized before configuration is loaded may use default settings. Lazy initialization or configuration-first startup sequences prevent this problem.

Summary

  • Singleton is an acceptable pattern for loggers because logging frameworks already use it internally
  • logging.getLogger() (Python), LoggerFactory.getLogger() (Java), ILoggerFactory (.NET) are built-in singleton registries
  • Dependency injection of logger interfaces is preferred in modern architectures for testability
  • Custom singleton loggers require thread-safe initialization (double-checked locking or framework-provided patterns)
  • For library code, use a logging facade (SLF4J) rather than a concrete singleton
  • Don't build a custom singleton logger when the standard library already provides one

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.