Intent
Ensure a class has only one instance, and provide a single global point of access to it.
The Problem
You've got a ConfigService. It loads a YAML file at startup, parses it, and exposes typed accessors. The first version gets instantiated in the bootstrap file:
$config = new ConfigService('/etc/app/config.yaml');
A month later, the config is needed in a controller — so someone instantiates it there too. And in a console command. And in a queue worker. Each of those reads the same file from disk, parses the same YAML, and produces a different in-memory instance. They drift the moment you call $config->set('feature_x', true) in one place and expect to read it from another.
Or take a database connection pool. Construct it twice and you have twice the connections. The pool was the whole point of the abstraction.
The shared property: there's a resource that should naturally have one and only one representation in the process. The straightforward way to enforce that is to put the rule inside the class itself.
The Solution
Singleton says: make the class enforce its own uniqueness. A private constructor stops anyone calling new directly. A static accessor returns the one shared instance, lazily creating it the first time.
final class ConfigService
{
private static ?self $instance = null;
private function __construct(private array $values) {}
public static function instance(): self
{
return self::$instance ??= new self(self::loadFromDisk());
}
}
Now there's no way to construct a second one by accident. ConfigService::instance() always returns the same object, from any caller, anywhere in the codebase.
Real-World Analogy
The President of a country. The constitution says there's one at a time — that's the rule, baked into the system itself, not an honor system. Anyone who asks for "the President" gets the same person, no matter where they're calling from. Even if a thousand people ask at once, there's still one President; the demand doesn't multiply the office.
When the term ends, a new President takes over (state changes), but at any moment there's still exactly one. That's the constraint Singleton encodes in code.
Structure
There are only two real moving parts:
- Singleton — the class. Has a private static field holding the single instance, a private constructor that only it can call, and a public static accessor (
instance(),getInstance(),default, etc.) that returns the shared instance. - Clients — anyone who needs the instance asks for it through the static accessor instead of calling
new.
The diagram is the simplest in this entire catalog. The complexity is in deciding when this is actually appropriate — and a lot of patterns lurk inside that decision.
Code Examples
Here's a minimal Singleton in five languages. Notice the seams: most languages need extra care for thread safety, and Go doesn't really have classes — its idiomatic version is a package-level variable behind a sync.Once.
export class ConfigService {
private static _instance: ConfigService | null = null;
private constructor(private readonly values: Record<string, unknown>) {}
static instance(): ConfigService {
if (!this._instance) {
this._instance = new ConfigService(this.loadFromDisk());
}
return this._instance;
}
private static loadFromDisk(): Record<string, unknown> { /* ... */ return {}; }
get<T>(key: string): T | undefined { return this.values[key] as T | undefined; }
}
// Use:
const config = ConfigService.instance();
class ConfigService:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._values = cls._load_from_disk()
return cls._instance
@staticmethod
def _load_from_disk():
# Read YAML, return dict.
return {}
def get(self, key):
return self._values.get(key)
# Use:
config = ConfigService() # always the same instance
public final class ConfigService {
private static volatile ConfigService instance;
private final Map<String, Object> values;
private ConfigService(Map<String, Object> values) {
this.values = values;
}
// Double-checked locking — thread-safe lazy init.
public static ConfigService instance() {
if (instance == null) {
synchronized (ConfigService.class) {
if (instance == null) {
instance = new ConfigService(loadFromDisk());
}
}
}
return instance;
}
private static Map<String, Object> loadFromDisk() { /* ... */ return new HashMap<>(); }
public Object get(String key) { return values.get(key); }
}
<?php
namespace App\Services;
final class ConfigService
{
private static ?self $instance = null;
private function __construct(private array $values) {}
public static function instance(): self
{
return self::$instance ??= new self(self::loadFromDisk());
}
private static function loadFromDisk(): array
{
// Read YAML, return array.
return [];
}
public function get(string $key): mixed
{
return $this->values[$key] ?? null;
}
}
// Use:
$config = ConfigService::instance();
package config
import "sync"
type ConfigService struct {
values map[string]any
}
var (
instance *ConfigService
once sync.Once
)
// Instance returns the process-wide ConfigService, lazy-initialised on first call.
// sync.Once guarantees the loader runs exactly once even with concurrent callers.
func Instance() *ConfigService {
once.Do(func() {
instance = &ConfigService{values: loadFromDisk()}
})
return instance
}
func loadFromDisk() map[string]any { /* ... */ return map[string]any{} }
func (c *ConfigService) Get(key string) any { return c.values[key] }
The Java version shows the thing nobody warned you about: writing a correct multi-threaded Singleton requires either synchronized (slow on every call) or double-checked locking with a volatile field (subtle, easy to get wrong). The Go version makes the same problem disappear with sync.Once, which is one of the reasons Go programmers rarely talk about Singleton as a "pattern" at all.
When to Use It
Reach for Singleton — the literal class-with-private-constructor pattern — only when you can answer "yes" to all of these:
- There is genuinely one shared resource at the process level. A connection pool, a thread pool, a write-through cache, a registry the kernel depends on. Not "a service that's currently used in three places."
- The instance has identity. Two
ConfigServiceobjects loaded from the same file aren't equivalent — they're two memory regions that will drift. The pattern enforces identity, not equivalence. - You can't or won't use a DI container. In a framework with a container (Laravel, Spring, NestJS, ASP.NET, Symfony), bind your service as a singleton in the container instead. Same outcome, vastly better testability.
- You're not afraid to argue for it in code review. Every Singleton in production code should have a comment explaining why it isn't just a DI binding.
For everything else — most things — use a regular class and let the container or the bootstrap code construct one and pass it where it's needed.
Pros and Cons
Pros
- Guaranteed single instance — the rule lives in the type, not in social agreement.
- Lazy initialization is straightforward.
- A single, well-known access point. Anyone can find it.
Cons
- Global mutable state in disguise. Once anything calls
Singleton::instance(), you've coupled it to the singleton with no parameter to swap or mock. - Testability suffers. You can't inject a fake. Most testing frameworks need either
@MockStatic-style hacks or a test-only reset method. - Thread safety is harder than the textbook suggests. Get the locking subtly wrong and you have a race condition that only shows up at scale.
- Hides dependencies. A method that calls
Singleton::instance()doesn't declare its dependency on it — it just reaches out and grabs it. - Strong coupling to a specific concrete class. Subclassing or replacing the singleton later is hard.
This is the GoF pattern with the most published criticism, and the criticism is fair. It earns its keep occasionally — and the rest of the time it's a polite way to write a global variable.
Pro Tips
- Default to "constructor injection," not Singleton. If a class needs config, take a
ConfigServicein its constructor. Let the container pick the lifetime. You get the same single instance and lose none of the testability. - If you must, expose a test-only reset. A
static resetForTests()method (compiled out of production builds, or explicitly documented) lets your tests start each case with a fresh singleton. - Prefer composition over
extends Singleton. Don't try to make a genericSingletonBaseclass — the resulting hierarchy is more pain than the duplication you avoid. - Watch out for "Singleton + lots of state." A Singleton that mostly returns immutable config values is benign. A Singleton that holds mutable state shared across requests is the source of the next bug.
Relations with Other Patterns
- Factory Method is sometimes the picker that returns the Singleton — but more often, it's the cleaner alternative. A factory that always returns the same instance does the same thing without the global static.
- Abstract Factory, Builder, Prototype are commonly implemented as Singletons themselves — the factory class itself only needs to exist once.
- Service Locator is the close cousin — a Singleton-shaped registry for many services. It has the same trade-offs (hidden coupling, hard to test) and is increasingly considered an anti-pattern in favor of constructor injection.
- Dependency Injection container is the modern replacement. The container is the registry of singletons; you bind interfaces to implementations and ask the container for instances. Same single-instance guarantee, vastly better testability and explicitness.
Final Tips
The first Singleton I shipped was a Logger class in a fresh codebase. Six months later we needed to write a test that asserted "this code should not log anything in this scenario." There was no way to swap the logger. I had to add a static setForTests() method, and the test suite slowly grew a thicket of "remember to reset the logger between tests." We eventually removed the Singleton and just injected the logger like any other dependency. Tests got cleaner, the logger stayed exactly as single in production, and life moved on.
That's the lesson. The pattern is fine in theory and occasionally correct in practice, but the trade-offs almost always tilt toward "use the container instead." Reach for the literal Singleton when you have a true single shared resource and no container — and reach for it knowing exactly what you're paying for.


