Design Patterns «Prev Next»

Lesson 6Singleton: participants and collaborations
ObjectiveExplain how Classes comprise a Singleton and Clients interface with Singleton.

Singleton Participants and Collaborations

This lesson explains two things: (1) what parts (“participants”) make up a Singleton implementation, and (2) how clients collaborate with the Singleton through its public interface. Although Singleton is structurally simple, modern best practice adds important constraints around concurrency, lifecycle boundaries, and testability.

Participants

Singleton is one of the simplest GoF patterns because the “pattern roles” are concentrated in a single class. You can think of its participants as a short list of responsibilities rather than a large cast of collaborating types.

  1. Singleton class (the role)
    The class that enforces the single-instance constraint and exposes the access point.
  2. Unique instance storage
    A class-level field (often private static) that holds a reference to the one instance.
  3. Access operation
    A public class-level method (commonly getInstance()) that returns the unique instance. Depending on the variant, it may also create the instance and enforce thread safety.
  4. Construction restriction
    Usually a private constructor to prevent external code from calling new and creating additional instances.

Modern clarification: “one instance” is almost always “one instance per runtime boundary” (per process, per container, per classloader). Singleton does not guarantee “only one” across a distributed system.

Collaborations

Singleton collaborations are intentionally minimal. The pattern’s collaboration rule is:

  1. Clients request the instance through the access method (for example, getInstance()), rather than constructing the object directly.

That single rule is what gives Singleton its characteristic collaboration shape: clients depend on the public access point and remain unaware of how the instance is created or stored.

A client is any object or class outside the pattern that uses the pattern’s public interface. Clients should not rely on a Singleton’s private implementation details (instance field, constructor mechanics, locking strategy, etc.).

Singleton with no subclassing

The simplest case is a Singleton that is not intended for subclassing. This is the most common modern approach, because many teams prefer composition over inheritance and treat Singleton as a lifecycle decision for a concrete service (for example, a process-wide registry).

The example below demonstrates the core collaboration: clients call a class method to obtain the one instance. However, as written, it is not thread-safe. That is acceptable for learning the relationships, but production code should use a thread-safe idiom (discussed next).

public class Singleton {
  private static Singleton uniqueInstance = null;

  private int data = 0; // instance attribute

  public static Singleton instance() {
    if (uniqueInstance == null) {
      uniqueInstance = new Singleton(); // lazy initialization
    }
    return uniqueInstance;
  }

  private Singleton() {}

  public int getData() { return data; }
  public void setData(int data) { this.data = data; }
}

Thread-safety note (modern best practice)

If two threads call instance() at the same time, the naive implementation can create multiple instances. Modern Java typically uses one of these approaches:

  • Initialization-on-demand holder idiom (lazy + thread-safe with minimal overhead)
  • Enum Singleton (robust against common serialization issues)
  • Double-checked locking with volatile (works, but more complex than necessary)

The key point for this lesson: regardless of the variant, the collaboration does not change. Clients still obtain the instance through the access method, not via construction.

Client interaction example

This test program illustrates the client collaboration. The important observation is that multiple requests return references to the same object.

public class TestSingleton {
  public static void main(String[] args) {

    // Get a reference to the single instance.
    Singleton s = Singleton.instance();

    // Set and read state through the Singleton instance.
    s.setData(34);
    System.out.println("First reference: " + s);
    System.out.println("Singleton data value is: " + s.getData());

    // Get another reference to the Singleton.
    // Is it the same object?
    s = null;
    s = Singleton.instance();

    System.out.println("\nSecond reference: " + s);
    System.out.println("Singleton data value is: " + s.getData());
  }
}

And typical output:

First reference: Singleton@1cc810
Singleton data value is: 34
Second reference: Singleton@1cc810
Singleton data value is: 34

Modern design guidance: “clients” and hidden dependencies

One reason Singleton is debated in modern design is that it can become a hidden global dependency: any client can “reach into” it at any time. If that makes testing or maintenance difficult, prefer an approach where:

  • Clients depend on an interface (for example, Registry or Config), and
  • The application’s composition root (or DI container) provides a single shared instance.

This preserves a “single instance per runtime” while keeping dependencies explicit. The collaboration becomes: client calls methods on an injected service instead of client locates a global instance.


SEMrush Software 6 SEMrush Banner 6