THIS EXPLANATION
THE ROOM
CDA·233 Computing, Data & AI 6 MIN · 8 STATIONS

The singleton regret

A Socratic walk-through of the singleton regret — reasoned out one step at a time, not lectured.

abcdefgh
a

The question we started with

THE QUESTION #

Why is the design pattern people learn first the one they later work hardest to remove?

Of the twenty-three patterns in the 1994 Design Patterns book, one is reliably the first anybody learns and the easiest to implement: ensure a class has only one instance, and give the world a way to reach it. It fits on a slide. It solves a problem everybody has had.

It is also the one that reliably appears on lists of things to avoid, and the one teams spend real effort removing years later. That combination is unusual. A pattern that was simply wrong would not have been adopted so widely; a pattern that was simply right would not generate so much rework. So the interesting question is not "is Singleton bad" but: what exactly is it that people regret, given that the thing it does is genuinely often needed?

b

Reasoning it through

REASONING #

Read the definition carefully, because it contains two promises, not one. Ensure a class has only one instance is a constraint on cardinality. Provide a global point of access to it is a decision about visibility. These are separate ideas, and the pattern welds them together.

Ask which of the two people actually wanted. Almost always the first: one connection pool, one configuration, one clock. That is a real requirement, and it is often true of the physical resource itself. Now ask which of the two causes the trouble. Watch what happens to a class that calls Logger.getInstance() in the middle of a method.

Its constructor no longer tells the truth. A reader, and a compiler, and a test, all see a class that needs nothing — while it in fact needs a configured logger to exist before it will work. The dependency is real but undeclared, which is the crux: dependencies you cannot see are dependencies you cannot substitute, cannot order, and cannot reason about locally.

Follow that into testing and the cost becomes concrete. To test the class in isolation you must arrange the global. Because the instance is static it outlives the test, so state set in one test leaks into the next, and the suite starts depending on execution order — the specific failure mode where each test passes alone and the run fails. And substituting a fake means reaching into the static field, which the pattern deliberately makes hard.

Then push on the word "one". One per what? The pattern says one per class loader or process, which is an implementation-level answer to what is really a business question. The moment the application needs one per tenant, one per request, or two for a migration, the constraint is in the wrong place — baked into the class rather than expressed by whoever assembles the system. That is the requirement that changes, and it changes often.

Concurrency deserves a mention, though it is the most fixable of the objections. Naive lazy initialisation is not thread-safe, and the double-checked locking idiom used to repair it was genuinely broken in Java before the memory model was revised in Java 5 (JSR-133). It works now, with volatile; an eagerly-initialised static field or an enum was always safer. So this part is a solved historical hazard rather than a live argument.

Which brings us to the reframing. Notice that everything painful traces to the access half, and everything valuable to the cardinality half. Separate them and both problems dissolve: let the class be an ordinary one, taking what it needs as a constructor parameter, and let the thing that assembles the application decide there will be exactly one. The single instance survives; the global reach does not. That is precisely what dependency-injection containers do with a "singleton scope" — the same lifetime, chosen by the composition root, with the dependency visible in every signature.

One honest qualification. Not every global is a defect. A genuinely stateless, immutable value — a mathematical constant, a pure formatter — accessed globally causes none of this, because nothing about it can be configured differently, corrupted between tests, or need a second variant. The regret attaches to mutable global state and to hidden dependencies, not to shared access as such.

c

The analogy

THE ANALOGY #
THE FIGURE

Think of a workshop with one master key hanging by the door, versus a workshop where each craftsman is handed the keys they need when they start a job. In both there is exactly one key to the store cupboard. But in the first, no job sheet ever records that the cupboard was opened, nobody can be given a duplicate for a job in a second building, and if someone leaves the key in a lock the next worker's job fails for reasons written down nowhere.

WHERE IT BREAKS DOWN

keys are physical and cannot be duplicated for free, whereas the object a singleton guards usually could exist twice at negligible cost — the constraint is nearly always a policy someone chose, not a fact about the resource, which is exactly why hard-coding it into the class ages so badly.

d

Clarifying the model

THE MODEL #

Three refinements.

First, the regret is not "one instance was the wrong number". It is usually the right number. The mistake is fixing that number in the class that provides the service rather than in the code that composes the application, where it can be changed without touching the service.

Second, a static accessor is not the only shape of the problem, and removing the keyword does not remove it. A service locator that any class may consult has exactly the same property: undeclared dependencies, resolved at run time, invisible in the signature. The tell is not static — it is whether a reader can see what a class needs by looking at how it is constructed.

Third, the pattern is not the same thing as a single physical resource. There is genuinely one file handle, one hardware device, one port. Owning that resource in one object is sensible; making that object globally reachable is a separate decision, and the one worth declining.

e

A picture of it

THE PICTURE #
The singleton regret
The singleton regret Read down the column of dates as a single decision compounding. Nothing goes wrong at the top -- the early entries are genuine benefits, which is why the pattern spreads. The turn comes when a second requirement arrives that the class already answered on everyone's behalf, and the final entry is the resolution: the count stays at one, but the choosing moves outward to the code that assembles the system. {"generator":"mermaid-svg-renderer@3.2.1","source":"../Socrates/.diagram-cache/_src/the-singleton-regret.md","sourceIndex":1,"sourceLine":4,"sourceHash":"76e523efb86b028c2495f978473750f31dd82f3984d30ad2ee5c5b1db5d6a4f6","diagramType":"timeline","layoutVariant":"source","repairedDuplicateIds":[],"motion":"entrance-with-reduced-motion-fallback","presentation":"editorial","attempt":1,"viewBox":{"x":0,"y":0,"width":1354,"height":677},"qa":{"passed":true,"findings":[]}} Week one Needed one loggereverywhere Static accessoradded in minutes No wiring, noparameters, works Month three Called from thirtyclasses No constructormentions it Convenient enoughto copy the trick Month six Unit tests need theglobal configured State leaksbetween tests Suite starts failingby run order Year one Second tenantneeds a secondinstance The number one isfixed in the class Concurrent inithazards surface Year two Dependency madean explicitparameter Lifetime moved tothe compositionroot Still exactly oneinstance, nowchosen

How to readRead down the column of dates as a single decision compounding. Nothing goes wrong at the top — the early entries are genuine benefits, which is why the pattern spreads. The turn comes when a second requirement arrives that the class already answered on everyone's behalf, and the final entry is the resolution: the count stays at one, but the choosing moves outward to the code that assembles the system.

f

What became clearer

WHAT CLEARED #
WHAT CLEARED

Singleton fuses two independent decisions — how many instances exist, and who may reach them — and only the second one is the source of the regret. Keeping the single instance while declining the global access point gives you everything the pattern was wanted for, with the dependency visible in the signature and the lifetime decided by whoever assembles the application rather than by the service itself.

g

Where to go next

ONWARD #
  • Composition roots, and why deciding lifetimes in one place resists this failure mode.
  • Whether a service locator is a genuine improvement over a static accessor or the same hazard renamed.
  • How immutability changes the argument for shared global access.
h

Key terms

TERMS #
TermWhat it means
Singletonthe pattern that ensures a class has exactly one instance and provides a global point of access to it.
Global statemutable data reachable from anywhere in a program without being passed in, and therefore changeable from anywhere.
Composition rootthe single place where an application wires its objects together and decides their lifetimes.
Double-checked lockinga lazy-initialisation idiom that was unsafe in Java before the JSR-133 memory model revision in Java 5.
Service locatora registry that classes consult at run time to obtain dependencies, keeping those dependencies out of their signatures.

Every term the collection defines is gathered in the glossary.

Nearby on the shelf

4