Singleton
The Singleton pattern ensures a class has only one instance and provides a global access point to that instance. Both parts matter: the enforcement (no new MyClass() elsewhere in the codebase), and the accessor (a getInstance() or equivalent that always returns the same object).
In practice, developers reach for the "global access" half and add the "single instance" constraint as a side effect. That conflation causes most of the problems associated with the pattern.
When it makes sense
Some things genuinely should exist exactly once per process: a configuration object loaded from environment variables at startup, a database connection pool shared across the application, a logger writing to a single file descriptor. Singleton semantics are appropriate here because multiple instances would either waste resources (pools are expensive to initialize) or produce incorrect behavior (two loggers writing interleaved lines to the same file).
The key phrase is "per process." A connection pool singleton in an application is fine. A connection pool singleton inside a library that other applications will import is a different situation — you've made a lifecycle decision on behalf of the library's callers.
Implementation
Go
Go has no classes, which strips the pattern down to its core. A package-level variable initialized with sync.Once is the idiomatic approach:
package config
import (
"os"
"sync"
)
type Config struct {
DatabaseURL string
Port string
}
var (
instance *Config
once sync.Once
)
func Get() *Config {
once.Do(func() {
instance = &Config{
DatabaseURL: os.Getenv("DATABASE_URL"),
Port: envOr("PORT", "8080"),
}
})
return instance
}
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
sync.Once is goroutine-safe. The initializer runs exactly once regardless of how many goroutines call Get() concurrently — no double-checked locking, no mutex to manage by hand.
For cases where initialization has no runtime dependencies and can't fail, a package-level var is simpler and equally safe:
var DefaultLogger = &Logger{output: os.Stdout, level: INFO}
Go's package initialization runs before main(), so this is safe as long as you're not dealing with circular package dependencies. If the initialization order between packages matters, prefer sync.Once.
TypeScript
ES modules are cached after first evaluation. Every import of the same module path gets the same binding, which makes a module-level export the simplest singleton implementation:
// config.ts
const config = {
databaseUrl: process.env.DATABASE_URL ?? '',
port: parseInt(process.env.PORT ?? '8080', 10),
environment: (process.env.NODE_ENV ?? 'development') as 'development' | 'production' | 'test',
} as const;
export default config;
Anywhere in the codebase: import config from './config'. The module evaluates once; every importer gets the same object.
When initialization is expensive and should be deferred until first use:
// pool.ts
import { Pool } from 'pg';
let pool: Pool | null = null;
export function getPool(): Pool {
if (!pool) {
pool = new Pool({ connectionString: process.env.DATABASE_URL });
}
return pool;
}
This is single-threaded (Node.js doesn't run JavaScript concurrently), so there's no race condition on the null check. If you're using worker threads and genuinely need synchronization, that's a different problem — but it's rare enough in Node.js that the simple null check is correct for the vast majority of cases.
The class-based version you'll find in many TypeScript tutorials — private static instance: Singleton, private constructor(), static getInstance() — is Java idiom transplanted verbatim into TypeScript. The language doesn't need it.
Python
Python's import system caches modules in sys.modules after the first import. Re-importing a module returns the cached version, which makes the module-level instance pattern work the same way as Go's package-level approach:
# config.py
import os
class _Config:
def __init__(self) -> None:
self.database_url: str = os.environ.get("DATABASE_URL", "")
self.port: int = int(os.environ.get("PORT", "8080"))
self.debug: bool = os.environ.get("DEBUG", "").lower() in ("1", "true", "yes")
config = _Config()
from config import config
print(config.database_url)
The underscore prefix on _Config signals that the class is an implementation detail — only config is part of the module's public interface. This is a convention rather than enforcement, but it's useful: anyone reading the code knows not to instantiate _Config directly.
For thread-safe lazy initialization — relevant when using threading but less critical in async frameworks that run on a single event loop:
import threading
from typing import Optional
_lock = threading.Lock()
_instance: Optional["ExpensiveResource"] = None
def get_resource() -> "ExpensiveResource":
global _instance
if _instance is None:
with _lock:
if _instance is None:
_instance = ExpensiveResource()
return _instance
The double-checked locking here — checking _instance before acquiring the lock, then checking again inside — avoids locking on every call once the instance exists. In CPython, the GIL makes the outer check safe, but the pattern is written to be correct regardless of GIL behavior.
What goes wrong
The Singleton's poor reputation is mostly earned by its mutable variant. The issues:
Global mutable state. Any code anywhere in the codebase can call Config.get() and get the same object. If that object is mutable, mutations from one part of the system are visible everywhere else. This is the definition of a global variable, dressed up in a design-patterns costume.
Testing becomes order-dependent. If test A modifies the singleton and test B expects a clean default state, test order matters — and test isolation breaks. The usual fixes are adding a reset() method to the singleton (awkward) or patching at the module level (reasonable in Python, but test-only behavior that doesn't exist in production code).
Dependencies disappear from function signatures. A function that calls Logger.get() internally depends on the logger, but that dependency is invisible at the call site. Callers can't see it; test setup has to know about it implicitly. Dependency injection puts this dependency in the function's signature or constructor, where it's explicit and replaceable.
None of this applies to immutable singletons. A config object loaded once from environment variables and never written to afterward has none of these problems. The criticism is about mutable shared state, not about the "one instance" guarantee itself.
A useful question before reaching for the pattern: can the singleton's fields be const/final/readonly after initialization? If yes, it's probably fine. If callers need to write to it, stop and ask whether a different design — a dependency-injected service, a context passed through the call stack, a message bus — handles the use case without the global-state consequences.
Language fit
| Language | How the pattern maps |
|---|---|
| Go | Package-level var or sync.Once; no class mechanics needed |
| TypeScript | ES module cache; module-level export is the natural shape |
| Python | sys.modules cache; module-level instance, _PrivateClass convention |
All three languages make the GoF class-based Singleton largely unnecessary for application code. The pattern survives in framework and library design, where you're building abstractions over a class hierarchy and genuinely need to enforce instantiation rules across boundaries you don't control. In application code, the language-idiomatic equivalents are cleaner.