| Lesson 6 | Singleton: participants and collaborations |
| Objective | Explain how Classes comprise a Singleton and Clients interface with Singleton. |
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.
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.
private static) that holds a reference to the one instance.
getInstance()) that returns the unique instance.
Depending on the variant, it may also create the instance and enforce thread safety.
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.
Singleton collaborations are intentionally minimal. The pattern’s collaboration rule is:
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.).
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; }
}
If two threads call instance() at the same time, the naive implementation can create multiple instances.
Modern Java typically uses one of these approaches:
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.
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
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:
Registry or Config), andThis 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.