| Lesson 10 | Singleton: related patterns |
| Objective | Describe how other Patterns work with or substitute for Singleton Patterns |
Through-line: Once you separate access, instance-count, and scope, the “right substitute” becomes obvious: choose the pattern that solves the specific concern you actually have.
Dependency Injection (DI) replaces “global access” with explicit wiring. You can still have a single instance, but it is owned by a composition root (or DI container), not discovered through a global singleton call. This improves testability (mock injection) and reduces hidden coupling.
// Example in Java (constructor injection)
public final class ReportService {
private final AuditSink sink;
public ReportService(AuditSink sink) {
this.sink = sink;
}
public void publish(String msg) {
sink.write(msg);
}
}
In DI frameworks, “singleton scope” is typically one instance per container—an important nuance in web apps and microservices.
If your core concern is “creation rules,” a Factory is often the correct tool. Factories centralize construction logic, vary implementations, and allow policies like caching, pooling, or version-based selection. A Factory can return a shared instance (singleton-like) or a fresh instance, depending on context.
This distinction matters: Singleton enforces uniqueness; Factory enforces how instances are created. In modern systems, you frequently combine them: a Factory object may be DI-managed as a singleton, while it creates many products.
Builders are a clean alternative when “singleton” is being used only to avoid repeating setup code. Instead of a single global instance, use a Builder to construct the complex object when needed, possibly with shared configuration injected into the builder.
Prototype is useful when construction is expensive and you want fast clones of a baseline object. It can coexist with Singleton if you maintain one “prototype registry” (possibly singleton-scoped) that provides clones to callers.
Monostate provides “singleton-like” behavior by sharing state across instances. This is a substitute when you want consistent shared state but do not want to enforce a single identity. The tradeoff remains: shared global state still complicates testing and concurrency unless carefully managed.
In JavaScript runtimes, modules are commonly used to provide a single exported instance. Node caches loaded modules, so repeated imports typically reuse the same exported object per process. In clustered or horizontally scaled deployments, this becomes “one per worker/service instance,” not one globally.
A frequent misuse of Singleton is as a “global message hub.” Observer (or broader publish/subscribe approaches) can be a better fit: publishers do not need a global singleton reference, and subscribers can register/unregister. If you still need a central event bus, make its lifecycle explicit (DI-managed) rather than a hard-coded singleton.
Service Locator provides global lookup similar to Singleton, but can map interfaces to implementations and vary behavior by configuration. It can be practical in legacy code, but it still hides dependencies and can preserve the testing problems of singleton access. If you use it, treat it as an interim step toward DI.
Through-line: After seeing the substitutes, the remaining question is why Singleton is so often chosen anyway—and which failure modes those substitutes are designed to prevent.
The practical takeaway: if you keep Singleton, keep it small, stable, and explicitly lifecycle-managed. Otherwise, prefer DI + clear scopes.
Through-line: With the pitfalls understood, the “best” use of Singleton usually becomes narrower: not as the product you reach for everywhere, but as an orchestrator that supports other patterns.
Patterns rarely appear alone. It is common for the factory object itself (Abstract Factory), a builder director, or a prototype registry to be created once and reused. In that design, Singleton is not the “product,” but the reusable orchestrator that produces products.
You may also see a behavioral pattern component implemented as “one per application boundary” (for example, a single Observer dispatcher, event bus, or coordinator). Modern implementations frequently express this as DI “singleton scope,” not a hard-coded global.
Through-line: Finally, once the design intent is clear, implementation details matter—especially in C++, where initialization and concurrency mistakes can silently break the “single instance” promise.
The historical pointer-based lazy singleton pattern is important to recognize, but in modern C++ it is usually replaced by safer initialization techniques. The key issues are thread safety and initialization order across translation units.
// Singleton.h
class Singleton {
public:
static Singleton& Instance() {
static Singleton instance; // initialized once, thread-safe (C++11+)
return instance;
}
int DoSomething();
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
#include <mutex>
#include <memory>
class Singleton2 {
public:
static Singleton2& Instance() {
std::call_once(flag_, [] { inst_.reset(new Singleton2()); });
return *inst_;
}
Singleton2(const Singleton2&) = delete;
Singleton2& operator=(const Singleton2&) = delete;
private:
Singleton2() = default;
static std::once_flag flag_;
static std::unique_ptr<Singleton2> inst_;
};
If other global/static objects depend on a singleton during startup, prefer the function-local static approach to reduce “initialization order” risk. If shutdown ordering matters, document it and consider explicit lifecycle management.