Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
⏱️ 6 min read

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


When Singleton Makes Sense

When it does NOT make sense:


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

  1. β€œHow do you make Singleton thread-safe?” – Meyer’s Singleton (C++11), double-checked locking with volatile (Java), threading.Lock (Python), or enum (Java).

  2. β€œ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.

  3. β€œ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.

  4. β€œ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.

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access