Ask a room of senior engineers to name the worst design pattern and someone will say Singleton before you finish the question. It has been called an anti-pattern, a code smell, and a lie told with a static method. The pile-on is mostly deserved. It is also mostly aimed at the wrong target.
My claim: Singleton bundles two separate ideas, and only one of them is a problem. The first idea is one instance: the process should hold exactly one connection pool, one configuration object, one clock. That is a fact about your system and it is often correct. The second idea is global access: any code, anywhere, can reach that instance by name. That is the part that wrecks your tests and your ability to reason about a class. Keep the first, drop the second, and most of the hatred goes away.
What the pattern says
The Gang of Four definition is short: ensure a class has only one instance, and provide a global point of access to it. Two requirements, joined by an "and". Almost every classic implementation delivers both through the same mechanism, a private constructor plus a static accessor:
public final class Config {
private static final Config INSTANCE = new Config();
private final Map<String, String> values;
private Config() { this.values = loadFromEnv(); }
public static Config get() { return INSTANCE; }
public String value(String key) { return values.get(key); }
}
Any class can now write Config.get().value("DB_URL") on line 40 of a method and nobody reading the constructor signature will know. That single line is the whole controversy.
Miško Hevery made the case in a 2008 Google Testing Blog post that remains the clearest statement of the problem. His argument is that singletons are global state, and global state makes an API lie about what it needs. A CreditCard class whose charge() method quietly reaches for a processor, an offline queue and a database looks like a simple object. Its constructor says nothing. Its tests only pass after you discover, through trial and error, that three other singletons must be initialized in a specific order. In his example the test even charges a real card on every run.
Picture a Clock.now() singleton used in 300 places. Writing a test for "the trial expires after 14 days" means mutating a process-wide variable, running the test, and remembering to reset it. Run the suite in parallel and one test's frozen clock leaks into another's assertions, so failures show up only on CI where the scheduling differs. Nothing is wrong with any single test. The shared mutable instance is the bug, and it is a very common shape of flaky-test report.
Hevery's fix is not "ban the single instance." It is to pass the dependency in. The class declares what it needs, the compiler enforces the order, and each declared dependency becomes a seam where a fake can be substituted.
# Hidden: the dependency is invisible from the outside
class TrialPolicy:
def is_expired(self, started_at):
return (Clock.now() - started_at).days > 14
# Declared: the dependency is the first thing you see
class TrialPolicy:
def __init__(self, clock):
self.clock = clock
def is_expired(self, started_at):
return (self.clock.now() - started_at).days > 14
In production you still build exactly one Clock at startup and hand it to everything. The program has a single instance. It just no longer has a global one.
Separating the two ideas
This is the move that makes the whole debate tractable. "Singleton" the pattern is a class that enforces its own uniqueness and advertises itself globally. "Singleton" the lifetime is a decision, made in one place, that this object is created once and shared. The second needs no special class at all.
Spring is the biggest production example of the second meaning. Its docs describe the singleton scope as the default, scoping a bean definition to a single object instance for each container. They then go out of their way to say this differs from the GoF pattern: the GoF version enforces one instance per class loader, while Spring's is per container and per bean, so one class can have several singleton beans. The class itself knows nothing about its own uniqueness. A composition root decides.
That is the version nearly every backend developer uses daily without a complaint: the database pool, the HTTP client, the Kafka producer, the metrics registry. They are all "one per process". None of them are reachable through a static accessor on their own class. They are injected.
When you really do need the class to guard itself
There are cases where a lifetime decision in a composition root is not enough, because the code that needs the instance has no composition root, or because creation is expensive and must happen lazily and exactly once under concurrency. The classic examples are a lazily built regex or template cache, a one-time native library initialization, and a process-wide logger in a library that cannot ask its callers to wire anything.
Here the interesting engineering is thread safety, and languages have converged on built-in primitives rather than hand-rolled double-checked locking. In Go, sync.Once is documented to perform exactly one action: Do calls the function if and only if it is the first call for that instance, and the return from the function synchronizes before the return from any call to Do. Since Go 1.21, sync.OnceValue wraps this for the common case of lazy values:
var loadConfig = sync.OnceValue(func() *Config {
return parseEnv()
})
func handler(w http.ResponseWriter, r *http.Request) {
cfg := loadConfig() // first call builds it, later calls return the same pointer
...
}
Two details from the docs matter in production. If the function panics, Once.Do considers it returned and future calls will not retry, so a failed initialization stays failed. OnceValue is stricter: it re-panics with the same value on every call. Neither retries. If your initializer can fail transiently, such as dialing a database, a plain sync.Once will cache the failure forever. Build the connection eagerly at startup and fail the process, or write an explicit retry guard.
In Java the same need is met by the initialization-on-demand holder idiom, where the JVM's class initialization rules give you lazy, thread-safe, lock-free construction:
public final class Registry {
private Registry() {}
private static class Holder {
static final Registry INSTANCE = new Registry();
}
public static Registry get() { return Holder.INSTANCE; }
}
Python takes yet another route: a module is executed once and cached in sys.modules, so a module-level object is a singleton for free. That is why Python codebases rarely need the pattern as a class.
The yes, but: "isn't injection just a global in a trench coat?"
The strongest objection is that if you inject the same Clock everywhere, you have a singleton with extra steps. In production behavior, yes. The instance count is one either way. The difference shows up in three places that matter.
First, the dependency is visible in the signature, so a reader knows what a class touches without reading its body. Second, a test can construct a fresh, isolated instance per test case, which makes parallel execution safe. Third, you can run two configurations in one process, for example two tenants with different settings, without rewriting anything. A true class-level singleton cannot do any of the three, because its uniqueness is welded into the type.
There is a cost, and it is worth stating honestly. Passing a logger or a clock through six layers of constructors is noise, and teams that follow the rule rigidly end up with constructors taking nine parameters. The pragmatic answer is that a dependency graph that big is itself a signal, and a DI container or a small composition root absorbs the wiring. For genuinely cross-cutting concerns like logging, many teams accept a global accessor and compensate by making it safe to use in tests. That is a deliberate trade, not a pattern violation, as long as it is a trade you made on purpose.
There is also a failure mode on the other side. Sonar ships a Java rule that flags singleton overuse as a code smell, which is a fair summary of the mainstream view. But the position "never use a singleton" is as lazy as "always use a singleton". Both skip the question of what this particular object's lifetime and visibility should be.
What to do on Monday
Search your codebase for static getInstance() or get() accessors and sort them into two piles. In the first pile, put the ones that hold mutable state or do I/O: clocks, configuration that changes at runtime, database handles, caches. These are the ones that will hurt your tests. Convert them to constructor-injected dependencies, keep the "one per process" lifetime, and move the creation to your application's entry point.
In the second pile, put the immutable, side-effect-free ones: compiled regexes, parsed static tables, a read-only feature definition. Leave them alone. A static accessor to a value that never changes is not global state in any sense that bites you.
Then ask one question in your next design review whenever someone proposes a Singleton: do we need exactly one, or do we need everyone to find it by name? Almost always the answer is the first. Give them the first and withhold the second.

