Skip to main content

Patternbeginner

Singleton

Overview

Singleton ensures a class has a single instance and provides a global point of access to it.

That sentence contains two independent promises, and it is joining them that makes the pattern problematic. Almost always you need one, not both.

Problem

The stated problem: some resources should exist only once — a connection pool, a configuration registry, a cache.

That is legitimate. What the pattern does wrong is solving uniqueness and global access with the same mechanism.

Uniqueness is a lifecycle decision. Global access is a visibility decision. When Configuration.getInstance() is scattered across three hundred places, you do not have a single instance — you have a hidden dependency in three hundred places, which no signature declares.

The consequences are well known and all derive from the global access, not from the uniqueness:

Invisible dependency. A method's signature does not reveal that it depends on configuration. Reading the code is not enough to know what it needs.

Coupled tests. Replacing the instance requires a global mechanism, and tests come to interfere with each other through execution order.

Shared state. A mutable singleton is a global variable with apparent encapsulation, with all the concurrency problems that implies.

Core Concepts

Separate the two decisions

If you need only one instance — configure that where the object is created. A dependency injection container does exactly that: application scope, one instance, injected into whoever needs it.

If you need convenient access — pass the dependency. A signature declaring (Configuration config) is honest about what the method needs.

Combining a single instance with explicit injection delivers the uniqueness and the testability Singleton promises, at the cost of the explicit wiring — which pays for itself in almost everything, and turns into disproportionate noise on the cross-cutting dependencies that run through the entire codebase.

A stateless singleton is less bad

An immutable, stateless object accessed globally does less damage — there is no race condition and no interference between tests.

There is still a hidden dependency, which is the structural cost. But the operational risk drops sharply.

The legitimate case

It exists: infrastructure entry points that the environment already treats as global — logging, metrics, the system clock.

Even there, the testable form is a global facade over an injectable instance.

When to Use

  • Resources the environment already treats as global — logging, metrics — and whose explicit passing throughout the codebase would be disproportionate noise.
  • Immutable, stateless objects.
  • When the language or framework imposes the mechanism.

When Not to Use

When you only need one instance. Use dependency injection scope. It is the answer in the overwhelming majority of cases.

For anything with mutable state. It is a global variable.

For database access, HTTP clients, repositories. They are business dependencies and should be declared in the signature.

When tests need to substitute it. If substituting requires a global mechanism, the test became coupled to execution order.

For configuration. It is the most common application and one of the worst: it spreads a hidden dependency across the whole system.

Alternatives

  • Dependency injection with application scope — uniqueness without global access. The main alternative.
  • An explicit parameter — pass the object.
  • A context object — group what crosses many layers.
  • A module with encapsulated state — in languages with first-class modules, it solves this without a class.

Trade-offs

SingletonInjection with single scope
Access from anywhereHas to be received
No wiringExplicit wiring
Invisible dependencyDeclared dependency
Tests need a global mechanismSubstitution trivial
Risk of shared stateControlled scope
One instance guaranteedOne instance configured

Singleton wins in two columns. One is convenience. The other is the strength of the guarantee: the uniqueness is structural, whereas an injection registration wrongly configured as transient hands out two instances without warning anyone. The second advantage is real and still does not compensate — registration configuration is verifiable in a test, and the coupling that global access creates is not verifiable anywhere.

Failure Modes

Shared state under concurrency. Race conditions in code that looks isolated.

Tests that interfere with each other. One test changes the singleton, another fails afterwards — and the failure depends on the order.

Unsafe lazy initialization. Two threads create two instances.

Initialization cycle. Two singletons referencing each other during construction.

Hidden dependency discovered late. A method that looked pure depends on configuration, and that only surfaces when the environment changes.

Common Mistakes

Using it for configuration. The most common.

Confusing uniqueness with global access. The central confusion.

Thinking the solution is making the singleton testable. If you need a mechanism to substitute it, the dependency should be explicit.

A mutable singleton. A global variable under another name.

Where it appears in practice

Logging registries. LoggerFactory.getLogger(...) is global access over a single configuration. It is the legitimate case, and it works because logging is cross-cutting infrastructure and the object is effectively stateless from the caller's point of view.

Dependency injection containers. Ironically, the container tends to be a singleton — and exists so that nothing else has to be.

Connection pools. One instance per application, but good libraries inject it rather than exposing static access.

The pattern in all three cases is the same: uniqueness yes, global access only when the object is cross-cutting and has no observable state. Mature libraries converged on that; application code frequently does not.

Real-World Example

A system had GlobalConfiguration.getInstance() called in 214 places.

Two problems appeared together.

Tests failed intermittently depending on order — a test that changed a parameter affected the following ones, and CI reordered them.

And nobody could say which parts of the system depended on which parameter, because the dependency was in no signature.

The migration was incremental: the class became injectable, getInstance() was kept as a temporary delegation, and the calls were replaced module by module.

The unexpected result came midway: on declaring the dependencies, the team discovered that 60% of the 214 points used only three parameters. Those three became method parameters, and most of the system stopped depending on configuration at all.

The Singleton was not just hiding a dependency — it was hiding that the dependency was far smaller than it appeared.

Practical Exercise

Count how many calls to static instance-access methods exist in your system.

Pick the most used one and list, for the first ten usage points, exactly what each of them consumes from it.

If most use few fields, the real dependency is smaller than the declared one — and can probably become a parameter.

Interview Questions

  • What are Singleton's two promises, and why is joining them a problem?
  • Why does Singleton make testing harder?
  • When is it acceptable?

Further Exploration

  • Gamma, Erich et al. Design Patterns. Addison-Wesley, 1994.
  • Fowler, Martin. Inversion of Control Containers and the Dependency Injection Pattern, 2004.
Finished reading this document?Your progress is saved in this browser only.