| Lesson 3 | Singleton: intent and motivation |
| Objective | Describe the Intent and Motivation for Singleton Patterns |
The Singleton is a creational design pattern used when a system needs exactly one instance of a class and a reliable way for collaborating objects to reach it. Conceptually, Singleton is simple: it restricts instantiation and centralizes access. Practically, it carries tradeoffs—especially around hidden dependencies, global mutable state, and testability—so modern best practice is to use it deliberately and sparingly.
The classic intent of Singleton is twofold:
Modern guidance adds one more lens: Singleton is often a global access mechanism, which can be useful, but it can also hide dependencies. If many parts of your code “reach into” a Singleton, your design may become harder to test, refactor, and reason about.
Singleton is motivated by problems where a system benefits from a single coordinating object:
A classic example is an AudioDevice abstraction. Many machines expose one active audio output pathway at a time.
If two AudioDevice objects compete to control the same underlying resource, you can get contention, race conditions,
and unpredictable behavior. A single instance can serialize access (for example, by queuing requests) and centralize device initialization.
Important modernization: “single instance” almost always means per process (or per application container), not “one in the universe.” In distributed systems, microservices, or multi-node deployments, each process may have its own Singleton instance. If you truly need a single active coordinator across nodes, you typically need an external mechanism (leader election, distributed lock, database constraint, etc.), not an in-process Singleton.
Singleton can appear in several domains where “one coordinator” is a reasonable design choice:
The key question is not “can I make this a Singleton?” but rather: does the system require a single authoritative coordinator, or am I using Singleton as a convenient global variable? If it’s primarily convenience, consider dependency injection (DI), passing interfaces explicitly, or using framework-managed lifecycles.
Question: How do we ensure that a class has only one instance and that the instance is easily accessible?
Answer: Make the class responsible for tracking its sole instance (typically via a private constructor and a static accessor),
prevent external instantiation, and provide a well-defined access method.
The mechanics vary by language. The implementation below illustrates the intent: prevent direct construction and expose one instance. In production code, prefer patterns that are naturally thread-safe and test-friendly (for example, DI-managed singletons or language idioms).
// Example (Java): Initialization-on-demand holder idiom (lazy, thread-safe)
public final class TrafficLightManager {
private TrafficLightManager() {
// private: prevents external instantiation
}
private static class Holder {
private static final TrafficLightManager INSTANCE = new TrafficLightManager();
}
public static TrafficLightManager getInstance() {
return Holder.INSTANCE;
}
// Keep responsibilities narrow: coordination, not "all application state"
public void synchronizeLights() {
// coordination logic
}
}
If you need to unit test code that depends on TrafficLightManager, consider defining an interface and injecting an implementation.
That preserves the “single instance per runtime” behavior while avoiding hardwired global coupling in consumers.
Later in this module, you will apply Singleton to guarantee that the traffic light manager has only one instance. That single coordinator can help ensure the lights remain in sync and that scheduling decisions are consistent.
Most patterns have multiple motivations; the value of a pattern is its adaptability across contexts. The discipline is knowing when the pattern improves clarity and correctness—and when it introduces unnecessary coupling.
The Singleton pattern’s role is to enforce a single instance and provide a stable access point to it. The class itself enforces the constraint (clients do not “behave correctly” by convention; the design makes incorrect construction impossible). When used well, Singleton centralizes coordination for a specific responsibility and avoids duplicate state.
Modern best practice is to treat Singleton as a lifecycle decision (one instance) rather than a global dependency shortcut. If a design starts to rely on “reaching into” the Singleton from everywhere, you will often get a cleaner architecture by switching to explicit dependency passing or a DI container that manages a singleton-scoped service.