Design Patterns: Singleton
The Singleton Pattern ensures that a class has only one instance throughout the lifetime of the application and provides a global point of access to that instance.
You’re deep in code, headphones on, you’re in the zone. Then, your boss, exuding a quiet arrogance glides over to your desk.
They lean in…
Woah there Bromeo, Brofessor, Brobi-wan Kenobi, Broda… what the hell are you doing? Singleton is an ANTI-PATTERN, bro. Don’t use it, my guy.
But what does your overly enthusiastic bro-boss mean?
Well… they do actually have a point.
The Singleton
Singletons are a creational design pattern that makes sure a class has only one instance and provides a global access point to it.
In theory, they seem harmless — even handy. But in practice? Singletons get overused, and what starts as a convenient shortcut often snowballs into an untestable, tightly-coupled mess.
That’s why many developers now consider them an anti-pattern.
Here’s where things start to fall apart:
Hidden Dependencies
Singletons effectively behave like global state—accessible from anywhere in your application. While this may seem convenient at first, it quickly leads to unpredictable behaviour. When multiple services interact with the same shared instance, it becomes difficult to trace changes, reproduce bugs, or maintain clear separation of concerns.
Hard to Test
They’re hard to mock, hard to isolate, and they tend to drag their global state into every test. Before long, you're writing setup/teardown code just to “reset the singleton,” which should be a red flag.
Tight Coupling
When everything relies on the Singleton, changing that class might be an uphill task. You can’t just swap it out or replace it with a new implementation. Everyone’s holding hands with it.
Concurrency Problems
In multithreaded environments improperly implemented singletons can cause race conditions or state corruption if multiple processes start using or mutating the shared instance.
It Can Serve A Purpose
When implemented with care, the Singleton can serve its purpose in certain situations where a single point of access to an object is necessary—such as logging or managing application-wide configurations.
Here's a clean implementation of a Singleton pattern for managing a configuration class:
class Config
{
private static ?Config $instance = null;
private array $settings = [];
private function __construct()
{
// Load the configuration from a file or environment
$this->settings = [
'db_host' => 'localhost',
'db_user' => 'root',
'db_pass' => 'secret'
];
}
public static function getInstance(): Config
{
if (self::$instance === null) {
self::$instance = new Config();
}
return self::$instance;
}
public function getSetting(string $key)
{
return $this->settings[$key] ?? null;
}
}
Not much to it, right? Here’s how we’d use it:
// Anywhere in the app, you can access config:
$dbHost = Config::getInstance()->getSetting('db_host');
$dbUser = Config::getInstance()->getSetting('db_user');
Here’s some benefits:
Lazy Initialisation: The
Config
class only initialises when it's first accessed. This avoids unnecessary instantiations.Controlled Access: The configuration is the same across the application, and no one can directly instantiate a
Config
object, ensuring the integrity of the settings.Clear Use Case: A single point of access to settings is a clear and valid use for the Singleton pattern.
The Problem Isn’t The Pattern
It’s how it’s used. It’s a tool. You wouldn’t try to wash the dishes with a loaded gun, would you?
Here’s a bad use of the Singleton pattern, one that's all too common in beginner projects (and more than a few legacy codebases): the Database connection.
Let’s create another Singleton:
class Database
{
private static ?Database $instance = null;
private PDO $connection;
private function __construct()
{
$this->connection = new PDO('mysql:host=localhost;dbname=test', 'root', 'secret');
}
public static function getInstance(): Database
{
if (self::$instance === null) {
self::$instance = new Database();
}
return self::$instance;
}
public function query(string $sql)
{
return $this->connection->query($sql);
}
}
Again, not much to it, right? Here’s how we’d use it:
// Anywhere in the app, you can access the db:
$users = Database::getInstance()->query('SELECT * FROM USERS');
Why is this a problem?
Hidden Dependencies: You’re not passing your database connection around—you’re reaching for it. It’s bad architecture.
Tight Coupling: You’ve now hardcoded your connection into this singleton, making it impossible to switch to a different driver, run tests, or even point to a different database in staging.
Unmockable in Tests: Want to run your tests without touching the real database? Good luck.
Instead of Singleton (in this example), consider dependency injection. Here’s a basic example of a UserRepository
that accepts a PDO
connection:
class UserRepository
{
public function __construct(private PDO $connection) {}
public function find(int $id)
{
$stmt = $this->connection->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $id]);
return $stmt->fetch();
}
}
Now you can pass in a mock connection, a real connection, or no connection at all (and let the container handle it). You’re not locked into a single static instance. You’ve got freedom and flexibility.
Final Thoughts: Singleton Isn’t Evil
It’s misunderstood. The Singleton pattern isn’t inherently wrong. It’s just frequently abused.
It’s great for things like config management, feature flags, or logging where global access to a consistent instance makes sense and doesn’t compromise your code’s testability or flexibility.
But for most other use cases? Reach for dependency injection, factories, or service containers.
So next time your bro-boss slides over and hits you with the "Singleton is an anti-pattern, bro!" — just nod, smile, tell them it depends on the context and show them your Config class (oi oi).
Want to see how to quickly knock up a Singleton? Take a look at my coding tips repository by clicking here. Fanks!
…and the git link actually mentions it… :-)
I don’t say I’m totally confused, but rather that in this case it’s possible to argue both sides of the coin. I suppose there’s a best practice to get the maximum out of such a scenario.
It seems that Prisma actually encourages the DB connection to be a singleton, to be able to make use of connection pooling. But it really seems a lot neater to use dependency injection. And then it looks kinda stoobid to pass around a singleton. Unless, for being able to test, of course. So what’s best, apart from not using Prisma, I wonder…?