Singleton Pattern
Singleton ensures a class has exactly one instance and provides a global point of access to it. The class itself controls instantiation, preventing multiple copies from being created.
Why this matters: Singleton is one of the most misused patterns. In interviews, youβll use it for genuinely global resources (connection pools, configuration managers, the parking lot itself). But using it everywhere signals you donβt understand dependency injection. Know when itβs appropriate and when itβs a crutch.
Prerequisites
- SOLID Principles β Singleton can violate SRP if overused
- Concurrency β Thread safety is the core challenge
When Singleton Makes Sense
- Connection pools β One pool shared across the application
- Configuration/Registry β Loaded once, read everywhere
- Logger β One logging pipeline
- The βsystemβ itself β In a Parking Lot problem, thereβs literally one ParkingLot
When it does NOT make sense:
- Services that could reasonably have multiple instances
- Anything that makes unit testing harder (most things)
- Objects youβre making Singleton βjust becauseβ you only need one right now
Thread-Safe Implementations
flowchart LR
A["Eager Init<br/>Simple but wastes memory if unused"]:::client
B["Double-Checked Locking<br/>Lazy and thread-safe"]:::service
C["Enum Singleton<br/>Best for Java - serialization-safe"]:::data
A --> B
B --> C
classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
// Approach 1: Eager initialization (simplest)
// Instance created at class loading time -- always thread-safe
public class ConnectionPool {
private static final ConnectionPool INSTANCE = new ConnectionPool(10);
private final List<Connection> pool;
private ConnectionPool(int size) {
pool = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
pool.add(createConnection());
}
}
public static ConnectionPool getInstance() {
return INSTANCE;
}
public Connection acquire() { /* ... */ }
public void release(Connection conn) { /* ... */ }
}
// Approach 2: Double-checked locking (lazy initialization)
// Instance created on first access -- saves memory if never used
public class ConfigManager {
private static volatile ConfigManager instance; // volatile is critical
private final Map<String, String> config;
private ConfigManager() {
config = loadFromFile("application.properties");
}
public static ConfigManager getInstance() {
if (instance == null) { // First check (no lock)
synchronized (ConfigManager.class) {
if (instance == null) { // Second check (with lock)
instance = new ConfigManager();
}
}
}
return instance;
}
public String get(String key) { return config.get(key); }
}
// Approach 3: Enum singleton (recommended for Java)
// Handles serialization, reflection attacks, and thread safety for free
public enum Registry {
INSTANCE;
private final Map<String, Object> services = new ConcurrentHashMap<>();
public void register(String name, Object service) {
services.put(name, service);
}
public <T> T lookup(String name, Class<T> type) {
return type.cast(services.get(name));
}
}
// Usage: Registry.INSTANCE.register("emailService", emailService);
import threading
# Approach 1: Module-level (Pythonic singleton)
# Python modules are loaded once -- the module IS the singleton
# config_manager.py
_config = None
def get_config():
global _config
if _config is None:
_config = _load_from_file("application.yaml")
return _config
# Approach 2: Thread-safe with lock (when you need a class)
class ConnectionPool:
_instance = None
_lock = threading.Lock()
def __new__(cls, *args, **kwargs):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, size: int = 10):
if self._initialized:
return
self._initialized = True
self._pool = [self._create_connection() for _ in range(size)]
self._available = list(self._pool)
self._pool_lock = threading.Lock()
def acquire(self) -> 'Connection':
with self._pool_lock:
if not self._available:
raise PoolExhaustedError()
return self._available.pop()
def release(self, conn: 'Connection'):
with self._pool_lock:
self._available.append(conn)
# Approach 3: Metaclass-based (cleanest for multiple singletons)
class SingletonMeta(type):
_instances = {}
_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Logger(metaclass=SingletonMeta):
def __init__(self):
self._log_file = open("app.log", "a")
def info(self, message: str):
self._log_file.write(f"[INFO] {message}\n")
#include <mutex>
#include <memory>
// Approach 1: Meyer's Singleton (C++11 thread-safe)
// Local static initialization is thread-safe since C++11
class ConnectionPool {
public:
static ConnectionPool& getInstance() {
static ConnectionPool instance(10); // Thread-safe in C++11+
return instance;
}
// Delete copy and move
ConnectionPool(const ConnectionPool&) = delete;
ConnectionPool& operator=(const ConnectionPool&) = delete;
ConnectionPool(ConnectionPool&&) = delete;
ConnectionPool& operator=(ConnectionPool&&) = delete;
Connection* acquire() {
lock_guard<mutex> lock(poolMutex_);
if (available_.empty()) throw PoolExhaustedException();
auto conn = available_.back();
available_.pop_back();
return conn;
}
void release(Connection* conn) {
lock_guard<mutex> lock(poolMutex_);
available_.push_back(conn);
}
private:
explicit ConnectionPool(int size) {
for (int i = 0; i < size; ++i) {
pool_.push_back(make_unique<Connection>());
available_.push_back(pool_.back().get());
}
}
vector<unique_ptr<Connection>> pool_;
vector<Connection*> available_;
mutex poolMutex_;
};
// Approach 2: Double-checked locking (pre-C++11 or explicit control)
class ConfigManager {
static ConfigManager* instance_;
static once_flag initFlag_;
map<string, string> config_;
ConfigManager() { config_ = loadFromFile("app.conf"); }
public:
static ConfigManager& getInstance() {
call_once(initFlag_, []() {
instance_ = new ConfigManager();
});
return *instance_;
}
string get(const string& key) const { return config_.at(key); }
};
ConfigManager* ConfigManager::instance_ = nullptr;
once_flag ConfigManager::initFlag_;
Why Double-Checked Locking Needs volatile (Java)
Without volatile, the JVM can reorder instructions during object construction. Thread A might write the reference to instance before finishing constructor initialization. Thread B sees a non-null instance but reads uninitialized fields.
The volatile keyword prevents this reordering, ensuring the object is fully constructed before any thread sees it.
When to Use vs When to Avoid
| Singleton is Appropriate | Singleton is an Anti-Pattern |
|---|---|
| Connection pool (bounded shared resource) | Service classes (use DI instead) |
| Configuration loaded once | Anything that needs per-test isolation |
| Hardware interfaces (printer spooler) | Objects with mutable state that tests need to reset |
| True global coordinator (the game board) | βI only need oneβ (thatβs not a reason for Singleton) |
Singleton vs Dependency Injection
In production code, DI frameworks (Spring, Guice, Dagger) manage object lifecycles. A class can be βsingleton-scopedβ in the DI container without using the Singleton pattern.
In machine coding rounds, Singleton is acceptable for the top-level system object. For services, prefer constructor injection.
Interview Questions
-
βHow do you make Singleton thread-safe?β β Meyerβs Singleton (C++11), double-checked locking with volatile (Java),
threading.Lock(Python), or enum (Java). -
βWhat problems does Singleton cause in testing?β β Global state persists between tests, making them order-dependent. You canβt inject mocks. Solutions: provide a
resetForTesting()method (hack) or use DI container with singleton scope instead. -
βCan Singleton be broken?β β In Java, via reflection (
setAccessible(true)on private constructor), deserialization (creates a new instance), or cloning. Enum singleton handles all three. -
βWhen would you NOT use Singleton in an interview?β β For any service that might benefit from multiple instances or that tests need to mock. Only use it for genuinely global, shared, expensive-to-create resources.
See It in Action
Used implicitly in most LLD problems where the βsystemβ itself (ParkingLot, VendingMachine) is a singleton.