Design Patterns, Explained Simply · Creational Patterns: Creating Objects Flexibly

Singleton (and Why It's Often an Anti-Pattern)

Guarantee one instance with global access. Learn how it works, why it makes code hard to test, and the usually better alternative: create one instance and pass it in.

The problem

An app needs one configuration object and one connection pool. Creating several pools would exhaust the database's connections, and two config objects could disagree. So a developer makes AppConfig a Singleton: a private constructor and a static instance() method that always returns the same object.

Months later the test suite is flaky. One test turns on debug mode through AppConfig.instance(), and a later test that expects debug to be off fails, but only when the tests run in that order. The singleton's state leaked between tests, because everything shares the same global object.

Real-world analogy

A country has one official government; "the government" always refers to the same one, and anyone can reach it. That's convenient, but when you want to rehearse a policy in a simulation, you can't swap in a practice government. Every experiment affects the real one.

Without the pattern

pool_a = ConnectionPool(size=20)   # created in the orders module
pool_b = ConnectionPool(size=20)   # created again in the reports module: now 40 connections

The pattern

  • Private constructor (or a convention not to call it) so other code can't create instances.
  • Static instance() method that creates the object on first use (lazy) and returns the same one afterwards.
  • Global access: anyone can call AppConfig.instance().

The guarantee of one instance is often fine. The global access is what causes the trouble: hidden dependencies, shared state between tests, and code you can't run with a different configuration.

The common alternative is to create one instance in the composition root and inject it (see the dependency-injection lesson). You still have exactly one, but dependencies are visible and tests can pass their own.

The code

A classic singleton, its test leak, and the injected alternativeTry it yourself
class AppConfig:
    _instance = None

    def __init__(self):
        self.debug = False

    @classmethod
    def instance(cls):
        if cls._instance is None:  # created lazily, on first use
            cls._instance = cls()
        return cls._instance


print("same object:", AppConfig.instance() is AppConfig.instance())


def test_enables_debug():
    AppConfig.instance().debug = True  # mutates global state


def test_expects_default():
    return "pass" if AppConfig.instance().debug is False else "FAIL (debug leaked from the previous test)"


test_enables_debug()
print("singleton test:", test_expects_default())


# Alternative: one instance created at startup and passed in
class Config:
    def __init__(self, debug=False):
        self.debug = debug


class Greeter:
    def __init__(self, config):  # dependency is visible
        self.config = config

    def greet(self):
        return "hello (debug)" if self.config.debug else "hello"


shared = Config()  # the app still creates exactly one
print("app:", Greeter(shared).greet())
print("test:", Greeter(Config(debug=True)).greet(), "| app config untouched:", shared.debug)

Output

same object: True
singleton test: FAIL (debug leaked from the previous test)
app: hello
test: hello (debug) | app config untouched: False

The singleton test failed because of an earlier test, which is exactly the flaky-test problem. With injection, the test used its own config and the app's single instance was untouched.

When to use it

  • There must be exactly one of something, creating more would be wrong, and it has little or no mutable state: a logger, a read-only configuration loaded once, a hardware interface.
  • In practice, prefer letting your DI container or composition root create one instance ("singleton scope" in Spring, app()->singleton() in Laravel) over writing the static-instance pattern by hand.

When not to use it

  • To get convenient global access. That is the main reason singletons hurt: hidden dependencies and shared state.
  • When tests need different configurations or fakes.
  • For anything with mutable state shared across requests or threads, unless you handle thread safety carefully.
  • In Python, a module is already a single instance; a module-level object is the idiomatic singleton.

You've already used it

  • Python: modules are imported once and cached in sys.modules; None, True and False are singletons.
  • Java: Runtime.getRuntime(); Spring beans are singleton-scoped by default (one per container, injected, not static).
  • Laravel: $this->app->singleton(...) registers one shared instance in the service container.

Key points

  • Singleton guarantees one instance and provides global access to it.
  • The global access causes hidden dependencies and leaking state between tests.
  • Prefer one instance created at startup and injected, or a container's singleton scope.
  • If you hand-write one in Java, make lazy creation thread-safe.
  • Further reading: Refactoring Guru: Singleton.

Practice questions

Why is the Singleton pattern often called an anti-pattern?

Answer

Its global access creates hidden dependencies (you can't see from a constructor what a class uses), shared mutable state that leaks between tests and requests, and code that can't easily run with a different configuration or a fake. The "one instance" guarantee is fine; the global access point is the problem.

What is a better alternative to a hand-written singleton?

Answer

Create one instance in the composition root (or register it with singleton scope in a DI container, like Spring's default scope or Laravel's singleton()) and inject it where needed. There is still only one, but dependencies are explicit and tests can supply their own.

How do you make a lazily created singleton thread-safe in Java?

Answer

Synchronize the accessor, use the initialization-on-demand holder idiom (a static nested class holding the instance, which the JVM initialises lazily and safely), or use a single-element enum. Double-checked locking needs a volatile field to be correct.

How is a singleton usually done in Python?

Answer

With a module: modules are imported once and cached, so a module-level object (config = Config()) is shared everywhere. Overriding __new__ works too, but it is rarely needed.

Open in the app to mark lessons complete, take notes and track your progress.