Design Patterns «Prev Next»

Lesson 1

Introduction to the Singleton Design Pattern

The Singleton pattern controls the creation of a class so that one instance is available through a known access point. The original Gang of Four intent is concise: ensure that a class has one instance and provide global access to it. The difficult engineering work begins after that sentence. A design must define what “one” means, who owns the lifecycle, how concurrent callers are handled, and whether global access is actually desirable.

Singleton is a creational pattern because it changes how an object is obtained. Clients do not call a public constructor. The class, runtime, or dependency container controls creation and returns the designated instance.

One Instance Within Which Boundary?

A Java static field normally provides one value per loaded class, not one value for an entire distributed system. That distinction matters:

  • One thread: ordinary object ownership may be enough; no global accessor is required.
  • One process and class loader: a conventional Java Singleton can enforce this boundary.
  • Multiple class loaders: application servers or plugin systems may load more than one copy of the class.
  • Multiple processes or containers: every process can have its own Singleton.
  • One logical owner across a cluster: this requires external coordination, such as a database constraint, lease, or leader-election service.

Singleton is therefore not a distributed locking mechanism. It is a local object-creation policy whose exact scope must be documented.

Core Structure

A typical implementation has three elements:

  1. A restricted constructor prevents ordinary client construction.
  2. A class-level field or runtime facility retains the designated instance.
  3. A class-level operation returns that instance.

In modern Java, the initialization-on-demand holder idiom provides lazy initialization without writing explicit locking code:

public final class SimulationClock {
    private SimulationClock() {
    }

    private static final class Holder {
        private static final SimulationClock INSTANCE =
                new SimulationClock();
    }

    public static SimulationClock getInstance() {
        return Holder.INSTANCE;
    }
}

Class initialization is synchronized by the Java Virtual Machine. The instance is created when Holder is first used, and all callers receive the same reference. This solves safe publication for the instance. It does not automatically make mutable operations inside SimulationClock thread-safe.

When Singleton Can Be Appropriate

Singleton is defensible when the uniqueness requirement belongs to the domain or platform and the lifecycle truly spans the application. Possible examples include:

  • a process-wide adapter for a device that only permits one local owner;
  • a registry that must coordinate one set of process-local callbacks;
  • a simulation clock whose state must be consistent for every component in one simulation process; or
  • an immutable catalog that is expensive to construct and intentionally shared.

Even these examples require scrutiny. A logging facade may be globally reachable, while its appenders and destinations are managed as normal dependencies. Database applications generally use a connection pool, not one database connection shared by every request. Configuration can often be loaded once and passed as an immutable value.

Costs Hidden by Convenient Access

A call such as SimulationClock.getInstance() is convenient, but it hides a dependency from constructors and method signatures. That can create several problems:

  • Global mutable state: one test or request can affect another through shared data.
  • Order dependence: behavior can depend on which caller initialized or modified the instance first.
  • Difficult substitution: clients cannot easily receive a fake clock, logger, or repository.
  • Lifecycle ambiguity: shutdown, cleanup, and reset responsibilities become unclear.
  • Concurrency exposure: every caller can reach the same mutable object, increasing the need for synchronization.

The private constructor is not the main risk. The larger risk is uncontrolled global access to mutable behavior.

Alternatives to Compare First

RequirementCandidate designReason to prefer it
One service instance per applicationDependency-injection container scopeCentral lifecycle with explicit client dependencies
Immutable shared valuesConstruct once and pass the valueNo hidden mutable state
Many reusable expensive resourcesObject poolBounded reuse rather than a single bottleneck
One owner across serversExternal lease or leader electionCoordinates beyond one process
Stateless utility behaviorStatic function or normal serviceNo artificial instance lifecycle

Module Roadmap

This module examines Singleton as a design decision rather than a code recipe. The lessons progress through:

  1. the standard elements used to document a pattern;
  2. Singleton intent, motivation, applicability, structure, participants, and collaboration;
  3. benefits, consequences, concurrency risks, and implementation variants;
  4. real-world uses, substitutes, and controlled-creation variations;
  5. the course-project simulation clock; and
  6. the question of whether a “perfect” Singleton exists.

Course-Project Connection

The traffic-signal simulation needs a consistent notion of simulated time. A process-local SimulationClock gives the module a concrete uniqueness requirement to evaluate. Later lessons ask whether static global access is necessary, how timing logic can be tested without waiting on real time, and how the design would change if several simulations ran in the same process.

Keep one principle in view throughout the module: prove the required uniqueness boundary before choosing Singleton. If ordinary ownership or dependency injection expresses the requirement more clearly, use the simpler design.

For a pattern-family reference, see the GOFPattern Singleton guide.

SEMrush Software 1 SEMrush Banner 1