Design Patterns «Prev Next»

Lesson 14

Singleton Design Pattern Conclusion

This module treated Singleton as a design decision, not merely as a class with a private constructor. The pattern controls creation so that one instance is available through a known access point. A sound application must also define the uniqueness boundary, lifecycle, concurrency policy, and cost of global access.

The central lesson is conditional: Singleton can be appropriate when one process-local owner is a real requirement, but it is harmful when it disguises ordinary dependencies as global state.

From Pattern Description to Design Decision

The lessons used the standard pattern elements to move from a vague desire for “one object” to a reviewable design:

  1. Intent: ensure one instance within a stated boundary and provide controlled access.
  2. Motivation: coordinate a resource or policy that genuinely requires one owner.
  3. Applicability: identify when uniqueness is part of the domain and when normal dependency management is enough.
  4. Structure: restrict construction, retain the designated instance, and expose an access operation.
  5. Participants and collaboration: define the Singleton's responsibilities and how clients obtain and use it.
  6. Consequences: weigh controlled creation against global state, hidden dependencies, testing difficulty, lifecycle coupling, and concurrency risk.

This vocabulary makes the design explainable. It also prevents an implementation technique from being mistaken for a complete architectural justification.

“One” Is Always Scoped

A conventional Java Singleton is generally one instance per loaded class. Separate class loaders or application processes can each create their own instance. A static field cannot enforce one owner across a cluster.

Before selecting the pattern, state the boundary explicitly:

  • one object per request, session, simulation, or application process;
  • one object per dependency-injection container;
  • one object per class loader; or
  • one logical owner across machines.

The last requirement needs external coordination such as a lease, database constraint, or leader-election service. Singleton alone cannot provide it.

Implementation Choices Solve Different Problems

Java approachUseful propertyPrimary caution
Eager static fieldSimple safe publication through class initializationConstructs the object when the class initializes
Synchronized accessorClear lazy initializationAcquires the class monitor on each call
Initialization-on-demand holderLazy initialization using JVM guaranteesStill exposes a global dependency
EnumConcise construction and serialization behaviorEnum-shaped API may not fit every design
Double-checked lockingAvoids locking after initializationRequires volatile and more difficult reasoning

Thread-safe creation does not make the Singleton's mutable fields thread-safe. Compound domain operations need their own locking, immutability, atomicity, or single-threaded processing policy.

Alternatives and Related Patterns

The module compared Singleton with designs that often express the real requirement more clearly:

  • Dependency injection can configure one application-scoped service while keeping dependencies visible.
  • Factory Method and Abstract Factory centralize creation without requiring a globally accessible instance.
  • Object Pool manages a bounded set of reusable resources when one object would become a bottleneck.
  • Multiton controls one instance per key rather than exactly one instance.
  • Modules and static functions can express stateless behavior without manufacturing an object lifecycle.
  • External coordination handles uniqueness that must span processes or hosts.

These are not automatic replacements. They are candidate designs to compare against the same forces and consequences.

Course Project: The Simulation Clock

The traffic-signal project uses a shared simulation clock to make the uniqueness discussion concrete. One clock can give every signal a consistent timeline within one simulation process. The design must still answer several questions:

  • Will the application ever run two simulations in the same process?
  • Can tests substitute a fake clock without changing global state?
  • Who starts, stops, and resets the clock?
  • Which operations must be safe when several signal components call them?

If multiple simulations are required, an injected clock owned by each simulation is a better model than one process-wide Singleton. This illustrates why requirements, not pattern enthusiasm, should determine the scope.

There Is No Perfect Singleton

Lesson 13 compared eager initialization, synchronized access, the holder idiom, enum construction, and double-checked locking. Each can be technically correct under stated conditions. None eliminates Singleton's architectural costs.

The word “perfect” is therefore replaced by a more useful standard: the implementation is robust when its uniqueness boundary is justified, publication is safe, mutable state has a concurrency policy, lifecycle behavior is explicit, and clients can be tested without uncontrolled shared state.

Final Decision Checklist

  1. What failure would occur if two instances existed?
  2. Within exactly which process, class loader, container, or distributed boundary must uniqueness hold?
  3. Does the design need lazy creation, or is eager initialization clearer?
  4. How are mutable invariants protected after the instance is created?
  5. Who owns initialization, failure recovery, cleanup, and shutdown?
  6. Can clients declare the dependency explicitly and substitute a test implementation?
  7. Would dependency injection, a factory, a pool, a Multiton, or external coordination fit better?

If these questions do not have precise answers, delay the pattern decision and keep the dependency explicit.

Check Your Understanding

The next module broadens the discussion from Singleton to other creational patterns and the different ways they separate clients from concrete construction decisions.

SEMrush Software