| Lesson 7 | Singleton: consequences |
| Objective | Discuss and Consider the Effects of using a Singleton. |
The Singleton pattern looks deceptively simple: “one instance plus a global access point.” The real engineering work is understanding the consequences—the tradeoffs that show up in real codebases: lifecycle boundaries (process vs. distributed), concurrency, hidden dependencies, testing friction, and evolving architecture.
In this lesson we review the most important positive and negative consequences, and how modern teams mitigate the risks while preserving the legitimate benefits of “a single shared instance.”
Foo.getInstance() hides the dependency. Over time, the codebase can become difficult to refactor because
many classes implicitly depend on the Singleton. This is one reason modern teams prefer dependency injection or explicit composition.
One positive consequence frequently cited in GoF discussions is that Singleton can control access to its sole instance.
Because the instance reference is private, all access flows through a public method (such as getInstance()).
That method can apply policy:
limiting concurrent use, counting callers, delaying access until the system is ready, or returning a proxy/wrapper.
This is not unique to Singleton—any well-designed class can enforce access rules. The distinctive difference is that Singleton concentrates this access policy into a single, globally reachable entry point.
Many older Java examples use a synchronized accessor to make lazy initialization safe:
public class Singleton {
private static Singleton instance;
protected Singleton() { }
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
This works, but it has a consequence: every call to getInstance() pays synchronization overhead.
In modern Java, teams often prefer alternatives that keep correctness while reducing contention,
such as the initialization-on-demand holder idiom or enum singletons.
GoF mentions that a Singleton can be subclassed so that an application can be configured with different behavior at runtime. This can be useful, but it introduces complexity because you must ensure that the “single instance” contract is not violated and that clients are not forced into unsafe casts.
In modern systems, the more common way to achieve “swap behavior” is: define an interface and let the composition root or DI container supply a single implementation instance. That preserves a single shared instance while avoiding inheritance constraints and class casting risks.
If you do support subclassing, document the boundary clearly: “one instance of the base type” vs. “one instance per subclass” are different semantics and lead to different behaviors.
Many of Singleton’s benefits come from “single shared instance,” not from “global access.” Modern architecture often keeps a single instance while avoiding the global accessor:
These options keep dependencies explicit, improve testability, and reduce the “global state” smell, while still supporting a single shared service when it is truly required.