Singleton and Object Pool

Topics Covered

The Singleton Pattern

How Singleton Works

Lazy vs. Eager Initialization

When Singleton Makes Sense

Singleton Pitfalls and Alternatives

The Global State Problem

Testing Becomes Painful

The Alternative: Dependency Injection

When Singleton Survives Scrutiny

Object Pool Pattern

The Checkout/Return Lifecycle

Pool Sizing

Beyond Database Connections

Managing Shared Resources

Decision Framework

Combining Patterns

Practice Problems

Some resources should exist exactly once in your entire application. A configuration manager that reads settings from a file, a logger that writes to a single log destination, or a metrics collector that aggregates counters: creating multiple instances of any of these leads to bugs. Two configuration managers might hold conflicting values. Two loggers might interleave output or fight over a file handle. The Singleton pattern exists to prevent this: it guarantees that a class has exactly one instance and provides a global access point to it.

Multiple callers all receiving the same singleton instance

How Singleton Works

The core mechanism has three parts: a private constructor that prevents outside code from calling __init__ directly, a class-level variable that stores the single instance, and a class method that returns that instance (creating it on the first call).

python
1class ConfigManager:
2    _instance = None
3
4    def __new__(cls):
5        if cls._instance is None:
6            cls._instance = super().__new__(cls)
7            cls._instance._settings = {}
8        return cls._instance
9
10    def get(self, key: str) -> str:
11        return self._settings.get(key)
12
13    def set(self, key: str, value: str) -> None:
14        self._settings[key] = value

Every call to ConfigManager() returns the same object. The first call creates it; every subsequent call skips creation and hands back the existing instance.

python
1config_a = ConfigManager()
2config_b = ConfigManager()
3
4config_a.set("db_host", "10.0.0.1")
5print(config_b.get("db_host"))  # 10.0.0.1
6
7print(config_a is config_b)     # True — same object

Lazy vs. Eager Initialization

The example above uses lazy initialization: the instance is created the first time someone asks for it. This is useful when creating the instance is expensive (loading a config file, opening a network connection) and you want to defer the cost until it is actually needed.

Eager initialization creates the instance at class load time, before any code requests it. In Python, you can achieve this by instantiating at module level:

python
1class Logger:
2    def __init__(self):
3        self._file = open("app.log", "a")
4
5    def log(self, message: str) -> None:
6        self._file.write(message + "\n")
7
8# Eager: created when the module is imported
9_logger = Logger()
10
11def get_logger() -> Logger:
12    return _logger

Eager initialization is simpler and avoids thread-safety issues (the instance exists before any concurrent code runs), but it pays the creation cost even if the singleton is never used.

Interview Tip

In Python, the module system itself acts as a natural singleton mechanism. When you import a module, Python caches it: every subsequent import returns the same module object. A module-level variable is effectively a singleton without any special pattern.

When Singleton Makes Sense

Use Singleton when all three conditions are true: the resource is genuinely one-of-a-kind (one configuration, one logger destination), creating multiple instances would cause bugs or conflicts, and the resource needs to be accessible from many parts of the codebase. If even one condition is missing, Singleton is probably the wrong choice.