Design Patterns, Explained Simply · Creational Patterns: Creating Objects Flexibly
Singleton (and Why It's Often an Anti-Pattern)
Guarantee one instance with global access. Learn how it works, why it makes code hard to test, and the usually better alternative: create one instance and pass it in.
The problem
An app needs one configuration object and one connection pool. Creating several pools would exhaust the database's connections, and two config objects could disagree. So a developer makes AppConfig a Singleton: a private constructor and a static instance() method that always returns the same object.
Months later the test suite is flaky. One test turns on debug mode through AppConfig.instance(), and a later test that expects debug to be off fails, but only when the tests run in that order. The singleton's state leaked between tests, because everything shares the same global object.
Real-world analogy
A country has one official government; "the government" always refers to the same one, and anyone can reach it. That's convenient, but when you want to rehearse a policy in a simulation, you can't swap in a practice government. Every experiment affects the real one.
Without the pattern
pool_a = ConnectionPool(size=20) # created in the orders module
pool_b = ConnectionPool(size=20) # created again in the reports module: now 40 connections
The pattern
- Private constructor (or a convention not to call it) so other code can't create instances.
- Static
instance()method that creates the object on first use (lazy) and returns the same one afterwards. - Global access: anyone can call
AppConfig.instance().
The guarantee of one instance is often fine. The global access is what causes the trouble: hidden dependencies, shared state between tests, and code you can't run with a different configuration.
The common alternative is to create one instance in the composition root and inject it (see the dependency-injection lesson). You still have exactly one, but dependencies are visible and tests can pass their own.
The code
class AppConfig:
_instance = None
def __init__(self):
self.debug = False
@classmethod
def instance(cls):
if cls._instance is None: # created lazily, on first use
cls._instance = cls()
return cls._instance
print("same object:", AppConfig.instance() is AppConfig.instance())
def test_enables_debug():
AppConfig.instance().debug = True # mutates global state
def test_expects_default():
return "pass" if AppConfig.instance().debug is False else "FAIL (debug leaked from the previous test)"
test_enables_debug()
print("singleton test:", test_expects_default())
# Alternative: one instance created at startup and passed in
class Config:
def __init__(self, debug=False):
self.debug = debug
class Greeter:
def __init__(self, config): # dependency is visible
self.config = config
def greet(self):
return "hello (debug)" if self.config.debug else "hello"
shared = Config() # the app still creates exactly one
print("app:", Greeter(shared).greet())
print("test:", Greeter(Config(debug=True)).greet(), "| app config untouched:", shared.debug)
Output
same object: True singleton test: FAIL (debug leaked from the previous test) app: hello test: hello (debug) | app config untouched: False
class AppConfig {
private static _instance: AppConfig | null = null;
debug = false;
private constructor() {}
static instance(): AppConfig {
if (AppConfig._instance === null) AppConfig._instance = new AppConfig(); // created lazily, on first use
return AppConfig._instance;
}
}
console.log("same object:", AppConfig.instance() === AppConfig.instance() ? "True" : "False");
function testEnablesDebug() {
AppConfig.instance().debug = true; // mutates global state
}
function testExpectsDefault() {
return AppConfig.instance().debug === false ? "pass" : "FAIL (debug leaked from the previous test)";
}
testEnablesDebug();
console.log("singleton test:", testExpectsDefault());
// Alternative: one instance created at startup and passed in
class Config {
constructor(public debug = false) {}
}
class Greeter {
constructor(private config: Config) {} // dependency is visible
greet() { return this.config.debug ? "hello (debug)" : "hello"; }
}
const shared = new Config(); // the app still creates exactly one
console.log("app:", new Greeter(shared).greet());
console.log("test:", new Greeter(new Config(true)).greet(), "| app config untouched:", shared.debug ? "True" : "False");
Output
same object: True singleton test: FAIL (debug leaked from the previous test) app: hello test: hello (debug) | app config untouched: False
public class Main {
static void testEnablesDebug() {
AppConfig.instance().debug = true; // mutates global state
}
static String testExpectsDefault() {
return !AppConfig.instance().debug ? "pass" : "FAIL (debug leaked from the previous test)";
}
public static void main(String[] args) {
System.out.println("same object: " + (AppConfig.instance() == AppConfig.instance() ? "True" : "False"));
testEnablesDebug();
System.out.println("singleton test: " + testExpectsDefault());
Config shared = new Config(false); // the app still creates exactly one
System.out.println("app: " + new Greeter(shared).greet());
System.out.println("test: " + new Greeter(new Config(true)).greet()
+ " | app config untouched: " + (shared.debug() ? "True" : "False"));
}
}
final class AppConfig {
private static AppConfig instance;
boolean debug = false;
private AppConfig() {}
static synchronized AppConfig instance() { // created lazily; synchronized for threads
if (instance == null) instance = new AppConfig();
return instance;
}
}
// Alternative: one instance created at startup and passed in
record Config(boolean debug) {}
class Greeter {
private final Config config;
Greeter(Config config) { this.config = config; } // dependency is visible
String greet() { return config.debug() ? "hello (debug)" : "hello"; }
}
Output
same object: True singleton test: FAIL (debug leaked from the previous test) app: hello test: hello (debug) | app config untouched: False
Run this on your computer — see “Get Started” for setup.
<?php
final class AppConfig {
private static ?AppConfig $instance = null;
public bool $debug = false;
private function __construct() {}
public static function instance(): AppConfig {
return self::$instance ??= new AppConfig(); // created lazily, on first use
}
}
echo "same object: ", AppConfig::instance() === AppConfig::instance() ? "True" : "False", "\n";
function testEnablesDebug(): void {
AppConfig::instance()->debug = true; // mutates global state
}
function testExpectsDefault(): string {
return AppConfig::instance()->debug === false ? "pass" : "FAIL (debug leaked from the previous test)";
}
testEnablesDebug();
echo "singleton test: ", testExpectsDefault(), "\n";
// Alternative: one instance created at startup and passed in
class Config {
public function __construct(public bool $debug = false) {}
}
class Greeter {
public function __construct(private Config $config) {} // dependency is visible
public function greet(): string { return $this->config->debug ? "hello (debug)" : "hello"; }
}
$shared = new Config(); // the app still creates exactly one
echo "app: ", (new Greeter($shared))->greet(), "\n";
echo "test: ", (new Greeter(new Config(true)))->greet(), " | app config untouched: ", $shared->debug ? "True" : "False", "\n";
Output
same object: True singleton test: FAIL (debug leaked from the previous test) app: hello test: hello (debug) | app config untouched: False
The singleton test failed because of an earlier test, which is exactly the flaky-test problem. With injection, the test used its own config and the app's single instance was untouched.
When to use it
- There must be exactly one of something, creating more would be wrong, and it has little or no mutable state: a logger, a read-only configuration loaded once, a hardware interface.
- In practice, prefer letting your DI container or composition root create one instance ("singleton scope" in Spring,
app()->singleton()in Laravel) over writing the static-instance pattern by hand.
When not to use it
- To get convenient global access. That is the main reason singletons hurt: hidden dependencies and shared state.
- When tests need different configurations or fakes.
- For anything with mutable state shared across requests or threads, unless you handle thread safety carefully.
- In Python, a module is already a single instance; a module-level object is the idiomatic singleton.
You've already used it
- Python: modules are imported once and cached in
sys.modules;None,TrueandFalseare singletons. - Java:
Runtime.getRuntime(); Spring beans are singleton-scoped by default (one per container, injected, not static). - Laravel:
$this->app->singleton(...)registers one shared instance in the service container.
Key points
- Singleton guarantees one instance and provides global access to it.
- The global access causes hidden dependencies and leaking state between tests.
- Prefer one instance created at startup and injected, or a container's singleton scope.
- If you hand-write one in Java, make lazy creation thread-safe.
- Further reading: Refactoring Guru: Singleton.
Practice questions
Why is the Singleton pattern often called an anti-pattern?
Answer
Its global access creates hidden dependencies (you can't see from a constructor what a class uses), shared mutable state that leaks between tests and requests, and code that can't easily run with a different configuration or a fake. The "one instance" guarantee is fine; the global access point is the problem.
What is a better alternative to a hand-written singleton?
Answer
Create one instance in the composition root (or register it with singleton scope in a DI container, like Spring's default scope or Laravel's singleton()) and inject it where needed. There is still only one, but dependencies are explicit and tests can supply their own.
How do you make a lazily created singleton thread-safe in Java?
Answer
Synchronize the accessor, use the initialization-on-demand holder idiom (a static nested class holding the instance, which the JVM initialises lazily and safely), or use a single-element enum. Double-checked locking needs a volatile field to be correct.
How is a singleton usually done in Python?
Answer
With a module: modules are imported once and cached, so a module-level object (config = Config()) is shared everywhere. Overriding __new__ works too, but it is rarely needed.
Open in the app to mark lessons complete, take notes and track your progress.