Singleton Pattern

6 min read

Reading Progress0%
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5

Singleton Pattern

1. What Is It?

The pattern ensures a class has exactly one instance throughout the lifetime of an application, and provides a single global access point to that instance. Without it, client code could accidentally create multiple independent instances of what should be a shared resource — imagine two separate logging systems each writing to the same file, or two cache managers each holding a divergent view of the data.

The pattern solves both problems in tandem: it prevents duplicate instantiation and centralizes access. The instance is typically created lazily (on first access) to avoid paying the initialization cost until it's actually needed.


QUICK CHECK

Your application uses an in-memory cache manager to store frequently accessed database query results. A developer notices that different parts of the codebase are each instantiating their own cache manager object. What is the most likely consequence of this design?

Choose one answer

2. How It Works

Step-by-step mechanics:

  1. Make the constructor private (Java) or override the creation mechanism (Python).
  2. Store the single instance in a private static field on the class itself.
  3. Expose a public static method (getInstance() / get_instance()) that creates the instance on first call and returns the cached one on every subsequent call.
  4. In multi-threaded environments, guard the first-creation check with a lock to prevent two threads from each seeing "no instance yet" simultaneously.

Python

from __future__ import annotations
import threading


class SingletonMeta(type):
    """Thread-safe metaclass that enforces single-instance creation."""

    _instances: dict[type, "SingletonMeta"] = {}
    _lock: threading.Lock = threading.Lock()

    def __call__(cls, *args, **kwargs):
        with cls._lock:
            if cls not in cls._instances:
                instance = super().__call__(*args, **kwargs)
                cls._instances[cls] = instance
        return cls._instances[cls]


class DatabaseConnection(metaclass=SingletonMeta):
    """A database connection pool — exactly one should exist per process."""

    def __init__(self, url: str) -> None:
        self.url = url
        self._connected = False

    def connect(self) -> None:
        if not self._connected:
            print(f"Connecting to {self.url}")
            self._connected = True

    def query(self, sql: str) -> list:
        print(f"Executing: {sql}")
        return []


# Usage
db1 = DatabaseConnection("postgres://localhost/mydb")
db2 = DatabaseConnection("postgres://localhost/mydb")
assert db1 is db2  # True — same object

Java

public final class DatabaseConnection {

    // volatile ensures the write to _instance is visible across threads immediately
    private static volatile DatabaseConnection instance;
    private final String url;
    private boolean connected;

    private DatabaseConnection(String url) {
        this.url = url;
    }

    public static DatabaseConnection getInstance(String url) {
        if (instance == null) {                        // first check (no lock)
            synchronized (DatabaseConnection.class) {
                if (instance == null) {                // second check (with lock)
                    instance = new DatabaseConnection(url);
                }
            }
        }
        return instance;
    }

    public void connect() {
        if (!connected) {
            System.out.println("Connecting to " + url);
            connected = true;
        }
    }

    public void query(String sql) {
        System.out.println("Executing: " + sql);
    }
}

// Preferred modern Java form — enum-based Singleton
public enum AppConfig {
    INSTANCE;

    private final String databaseUrl = System.getenv("DB_URL");

    public String getDatabaseUrl() { return databaseUrl; }
}

// Usage
AppConfig config = AppConfig.INSTANCE;

The Java enum form is thread-safe by JVM specification, serialisation-safe without extra code, and immune to reflection-based instantiation attacks.


QUICK CHECK

A backend service uses a Singleton database connection pool. During startup, two threads both call getInstance() at the same moment before any instance exists. Without any locking mechanism, what is the most likely risk?

Choose one answer

3. Variants & Comparisons

ApproachHow It WorksProsConsBest For
Lazy initialization (basic)Create on first getInstance() callSimple; no upfront costNot thread-safeSingle-threaded apps
Double-checked lockingNull-check outside + inside synchronizedThread-safe; low lock overheadvolatile required; verboseMulti-threaded Java
Eager initializationCreate instance in static field initializerThread-safe by JVM; simpleInstance created even if never usedCheap-to-create resources
Enum Singleton (Java)JVM manages a single enum constantThread-safe; serialization-safe; reflection-safeJava only; can feel unnaturalJava applications
Metaclass (Python)Override type.__call__ to cache instancesTransparent to callers; inheritableMetaclass conflicts are possiblePython applications
Module-level variable (Python)Python module is imported once; store instance at module scopeIdiomatic Python; zero boilerplateSlightly less encapsulatedPython scripts/services

QUICK CHECK

A Java backend service needs a singleton database connection pool that is accessed frequently from multiple threads. The connection pool is somewhat expensive to initialize, so you want to avoid creating it if it's never used. Which singleton implementation strategy best fits this scenario?

Choose one answer

4. When to Use It (and When NOT To)

Use when:

  • Exactly one shared resource must coordinate access across the system: logging, configuration, connection pools, device drivers, caches.
  • You need a controlled access point, not just a convenient global.

Do NOT use when:

  • You're reaching for it just to avoid passing a dependency — use instead.
  • You need testability — Singletons are hard to replace with mocks; they carry global between tests.
  • The "one instance" constraint is an assumption, not a hard requirement — requirements change (multi-tenant systems, test isolation).

Anti-patterns:

  • Singleton as a service locator — hiding all dependencies behind a Singleton registry couples everything together invisibly.
  • Mutable global — a Singleton holding mutable data creates hidden ; concurrent mutations require careful locking.
  • Overuse — using Singleton for every utility class turns object-oriented code into disguised global state.

Decision triggers:

  • "This resource must be initialized once and shared everywhere" → consider Singleton
  • "I just want easy access to this object" → use instead
  • "I need one instance per thread or per request" → use thread-local storage or scoped DI containers

QUICK CHECK

A developer is building a web application and notices they need to access a UserService object in many different classes. To avoid passing it through multiple constructors, they decide to make UserService a Singleton. What is the main problem with this approach?

Choose one answer

5. Real-World Usage

Java java.lang.Runtime Runtime.getRuntime() returns the single Runtime instance representing the JVM process. There is exactly one runtime per JVM — the pattern enforces this constraint at the API level.

Spring Framework ApplicationContext Spring beans are -scoped by default (@Scope("singleton")). The ApplicationContext acts as a managed Singleton registry — it creates each bean once and returns the same instance on every injection. Spring takes the management burden off the developer while preserving the one-instance guarantee.

Python logging module logging.getLogger("myapp") always returns the same Logger instance for the same name. The logging module uses a module-level dictionary to cache loggers — effectively a Singleton registry — so that all code sharing a logger name sees the same configuration and handlers.


QUICK CHECK

Your team is building a large backend application where multiple modules each call logging.getLogger('orders') at startup. A junior developer suggests that this will create multiple separate logger instances and could lead to inconsistent log configurations across modules. What actually happens, and why?

Choose one answer

6. Interview Cheat Sheet

Key sentences to demonstrate depth:

  1. " combines two responsibilities — controlling instantiation and providing global access — which technically violates SRP, so I'd use it only when both constraints are genuinely required."
  2. "In Java, I prefer the enum form because the JVM guarantees a single enum constant, making it serialization-safe and reflection-safe without any extra code."
  3. "In Python, a metaclass override is cleaner than overriding __new__ because it doesn't interfere with the class's own __init__ semantics."
  4. "The biggest cost of is testability — global bleeds between tests unless you add a reset mechanism or inject the singleton as a dependency."

Common follow-up questions:

  • "How do you make a Singleton thread-safe in Java?" → Use double-checked locking with volatile, or the enum form which is JVM-guaranteed thread-safe.
  • "What's the difference between Singleton and a static class?" → A Singleton can implement interfaces, be subclassed, and passed as a dependency; a static class cannot. Singletons can be replaced with polymorphic alternatives; static classes cannot.
  • "How do you test code that depends on a Singleton?" → Either extract an the Singleton implements and inject a mock, or add a package-private resetInstance() method for tests.

Connections to other concepts:

  • Monostate pattern — alternative where multiple instances share the same via static fields; avoids the enforcement mechanism but achieves the same effect.
  • Registry pattern — a generalization: one instance per key, not one instance total. Often built using the same caching technique.
  • — pairing Singleton with an allows the one instance to be replaced by a test double, restoring testability.
Glossary History

Click dotted jargon to save explanations here.

Glossary History

Click dotted jargon to save explanations here.