Inline Singleton

Symptom

A class is wrapped in Singleton machinery (`getInstance`, private constructor, static cache) but the singleton-ness is not load-bearing. Callers reach into the global accessor; tests can't isolate the class because the singleton instance leaks across test cases; dependencies are invisible from constructor signatures.

Goal

The class is a regular collaborator that callers receive through their constructors (or another explicit hand-off). Tests construct fresh instances per case; dependencies are visible at the type level; global state shrinks.

Before the refactoring

// Singleton machinery for what is effectively a regular collaborator.
class Logger {
static instance = null;
static getInstance() {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
constructor() {
this.entries = [];
}
log(message) {
this.entries.push({ time: Date.now(), message });
}
}
// Client code reaches into the global accessor.
function processOrder(order) {
Logger.getInstance().log(`Processing order ${order.id}`);
// ...
}

After the refactoring

// Regular class; callers receive a logger through their constructor.
class Logger {
constructor() {
this.entries = [];
}
log(message) {
this.entries.push({ time: Date.now(), message });
}
}
class OrderProcessor {
constructor(logger) {
this.logger = logger;
}
processOrder(order) {
this.logger.log(`Processing order ${order.id}`);
// ...
}
}
Example source: Illustrative example written for this site, adapted from Kerievsky's pattern description in Refactoring to Patterns (Addison-Wesley, 2004), chapter 5. The book demonstrates inlining a configuration Singleton; this JavaScript version inlines a Logger Singleton in favour of constructor injection — same payoff: testability and explicit dependencies.
Pressure

Singletons hide dependencies and leak state across tests. The static cache survives between test cases, producing flaky failures that depend on test ordering. New callers couple to the global accessor instead of declaring what they need.

Tradeoff

Inlining a Singleton pushes dependency wiring outward — every caller now needs to know how to obtain the collaborator. Without a composition root, the wiring can scatter; reasonable Singletons (true cross-cutting infrastructure like a thread-safe metrics registry) may legitimately want the global access pattern.

Relief

Tests construct the class with stub collaborators; production wiring lives at the composition root; the dependency graph reads from constructor signatures. The static-cache class of flakiness disappears.

Trap

Inlining a Singleton without setting up a composition root leaves every caller responsible for constructing or finding the collaborator. The result can be worse than the Singleton — dozens of `new Logger()` instances each with their own state, accidentally fragmenting what was conceptually one log.