Loading learning content...
Having established that certain classes genuinely require exactly one instance, we now face the implementation challenge: how do we enforce this constraint in code?
The classic Singleton pattern provides an elegant solution built on two fundamental mechanisms:
newThis combination transfers instantiation control from the calling code to the class itself. The class becomes the gatekeeper of its own instances, guaranteeing that exactly one exists throughout the application lifetime.
By the end of this page, you will understand the complete structure of the classic Singleton pattern, implement it correctly in multiple languages, distinguish between lazy and eager initialization strategies, and recognize the design decisions embedded in the pattern.
The Singleton pattern consists of three essential elements that work together to enforce single-instance semantics:
1. Private (or Protected) Constructor
The constructor is marked private, preventing any code outside the class from calling new ClassName(). This is the enforcement mechanism—not documentation, not convention, but a compile-time guarantee that external instantiation is impossible.
2. Static Instance Variable
A private static field holds the single instance. Being static, it belongs to the class rather than any instance, meaning it exists even before any instance is created. This is where the "singleton" lives.
3. Public Static Accessor Method
A static method (often named getInstance(), instance(), or similar) provides the global access point. This method checks if an instance exists, creates one if not, and returns it. All code that needs the singleton accesses it through this method.
How the Elements Collaborate:
Singleton.getInstance() (cannot call new Singleton())getInstance() checks if instance is nullThe private constructor is key: only code inside the class can call it, and we write the class to call it exactly once.
The most common Singleton implementation uses lazy initialization: the instance is created on first access, not at class loading time. This defers the cost of initialization until the singleton is actually needed.
Let's implement a complete Singleton for our configuration manager example:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
/** * ConfigurationManager - A Singleton for application configuration * * Demonstrates the classic lazy-initialization Singleton pattern. * The instance is created on first access to getInstance(). */class ConfigurationManager { // Step 1: Private static field holds the single instance private static instance: ConfigurationManager | null = null; // Private instance state private settings: Map<string, string>; private loadedAt: Date; // Step 2: Private constructor prevents external instantiation private constructor() { console.log("ConfigurationManager: Initializing (this happens only once)"); this.settings = new Map(); this.loadedAt = new Date(); // Simulate expensive initialization this.loadFromSources(); } // Step 3: Public static accessor provides controlled access public static getInstance(): ConfigurationManager { // Create instance on first call if (ConfigurationManager.instance === null) { console.log("ConfigurationManager: First access, creating instance"); ConfigurationManager.instance = new ConfigurationManager(); } // Return the existing instance return ConfigurationManager.instance; } // Expensive initialization logic private loadFromSources(): void { // Load from configuration file this.settings.set("app.name", "MyApplication"); this.settings.set("app.version", "1.0.0"); // Load from environment variables this.settings.set("database.host", process.env.DB_HOST || "localhost"); this.settings.set("database.port", process.env.DB_PORT || "5432"); // Load from remote configuration service // this.fetchRemoteConfig(); console.log(`ConfigurationManager: Loaded ${this.settings.size} settings`); } // Public instance methods public get(key: string): string | undefined { return this.settings.get(key); } public get uptime(): number { return Date.now() - this.loadedAt.getTime(); } // Demonstrates that this is the same instance everywhere public getInstanceId(): string { return `Instance created at ${this.loadedAt.toISOString()}`; }} // ============================================// Usage demonstrating singleton behavior// ============================================ // First access - instance is createdconst config1 = ConfigurationManager.getInstance();console.log(config1.getInstanceId()); // Instance created at 2024-01-15T10:30:00.000Z // Second access - same instance returnedconst config2 = ConfigurationManager.getInstance();console.log(config2.getInstanceId()); // Same timestamp! // Proof they're identicalconsole.log(config1 === config2); // true // Any component can access the same configurationfunction someDeepFunction() { // No need to pass config through parameters const config = ConfigurationManager.getInstance(); return config.get("database.host");} // Trying to create new instance directly fails at compile time:// const badConfig = new ConfigurationManager(); // Error: Constructor of class 'ConfigurationManager' is privateLazy initialization means the configuration isn't loaded until someone actually needs it. If a short-running script only uses logging (not configuration), the expensive file loading never occurs. This can significantly improve startup time for applications with multiple singleton subsystems.
An alternative to lazy initialization is eager initialization: creating the instance when the class is loaded, before any code requests it.
When to prefer eager initialization:
When to prefer lazy initialization:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
/** * EagerConfigurationManager - Eager initialization variant * * The instance is created immediately when the class is loaded, * not on first access. Simpler but less flexible. */class EagerConfigurationManager { // Instance is created immediately with the class private static readonly instance: EagerConfigurationManager = new EagerConfigurationManager(); private settings: Map<string, string>; private constructor() { console.log("EagerConfigurationManager: Initializing at class load time"); this.settings = new Map(); this.loadFromSources(); } // Accessor is simpler - just returns the already-created instance public static getInstance(): EagerConfigurationManager { return EagerConfigurationManager.instance; } private loadFromSources(): void { this.settings.set("app.name", "MyApplication"); console.log("EagerConfigurationManager: Settings loaded"); } public get(key: string): string | undefined { return this.settings.get(key); }} // Instance is created when this module is first imported,// even before getInstance() is calledconsole.log("Module loading..."); // This line causes the class to be evaluated, triggering constructorconst config = EagerConfigurationManager.getInstance(); // Output:// Module loading...// EagerConfigurationManager: Initializing at class load time// EagerConfigurationManager: Settings loaded| Aspect | Lazy Initialization | Eager Initialization |
|---|---|---|
| Creation timing | First call to getInstance() | Class loading / program startup |
| Startup cost | Deferred until needed | Paid upfront |
| Thread safety | Requires synchronization | Inherently thread-safe in most languages |
| Initialization failures | Surfaced on first access | Prevents application from starting |
| Resource availability | Can wait for resources to exist | Resources must exist at class load time |
| Code complexity | Slightly more complex | Simpler |
| Memory usage | Memory used only when accessed | Memory used from startup |
The Singleton pattern adapts to different languages while maintaining the same core principles. Let's examine idiomatic implementations in several languages:
Java Implementation
Java's class loading guarantees make eager initialization particularly clean:
123456789101112131415161718192021222324252627282930
public class ConfigurationManager { // Eager initialization - class loader guarantees thread safety private static final ConfigurationManager INSTANCE = new ConfigurationManager(); private Map<String, String> settings; // Private constructor private ConfigurationManager() { this.settings = new HashMap<>(); loadConfiguration(); } // Public accessor public static ConfigurationManager getInstance() { return INSTANCE; } private void loadConfiguration() { settings.put("app.name", "MyApplication"); // Load from resources... } public String get(String key) { return settings.get(key); }} // Usage:// ConfigurationManager config = ConfigurationManager.getInstance();// String name = config.get("app.name");Python Implementation
Python uses module-level variables and the __new__ method for singleton behavior:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
class ConfigurationManager: """ Python Singleton using __new__ method override. __new__ is called before __init__ and controls instance creation. We intercept it to return the existing instance if one exists. """ _instance = None _initialized = False def __new__(cls): # If no instance exists, create one if cls._instance is None: print("ConfigurationManager: Creating singleton instance") cls._instance = super().__new__(cls) return cls._instance def __init__(self): # Prevent re-initialization on subsequent calls if ConfigurationManager._initialized: return print("ConfigurationManager: Initializing") self._settings = {} self._load_configuration() ConfigurationManager._initialized = True def _load_configuration(self): self._settings["app.name"] = "MyApplication" self._settings["app.version"] = "1.0.0" def get(self, key: str) -> str | None: return self._settings.get(key) # Usage:config1 = ConfigurationManager() # Creates and initializesconfig2 = ConfigurationManager() # Returns same instance, skips init print(config1 is config2) # Trueprint(config1.get("app.name")) # MyApplication # Alternative: Module-level singleton (more Pythonic)# Just create instance at module level:# _config_instance = ConfigurationManager()# def get_config() -> ConfigurationManager:# return _config_instanceC# Implementation
C# offers multiple approaches, with Lazy<T> providing thread-safe lazy initialization:
12345678910111213141516171819202122232425262728293031323334
public sealed class ConfigurationManager{ // Thread-safe lazy initialization using Lazy<T> private static readonly Lazy<ConfigurationManager> _instance = new Lazy<ConfigurationManager>(() => new ConfigurationManager()); private readonly Dictionary<string, string> _settings; // Private constructor private ConfigurationManager() { Console.WriteLine("ConfigurationManager: Initializing"); _settings = new Dictionary<string, string>(); LoadConfiguration(); } // Public accessor - Lazy<T>.Value handles thread safety public static ConfigurationManager Instance => _instance.Value; private void LoadConfiguration() { _settings["app.name"] = "MyApplication"; _settings["app.version"] = "1.0.0"; } public string? Get(string key) { return _settings.TryGetValue(key, out var value) ? value : null; }} // Usage:// var config = ConfigurationManager.Instance;// var name = config.Get("app.name");The static accessor method serves as the controlled gateway to the singleton instance. Several design decisions affect its usability:
Naming Conventions
Different naming conventions have emerged across communities:
| Convention | Example | Common In |
|---|---|---|
getInstance() | ConfigManager.getInstance() | Java, TypeScript, traditional OOP |
instance property | ConfigManager.instance | C#, Kotlin, modern languages |
shared | ConfigManager.shared | Swift (iOS/macOS development) |
current | ConfigManager.current | C# (when representing current state) |
default | ConfigManager.default | When there are multiple configurations |
| Module function | getConfigManager() | Python, JavaScript modules |
Method vs Property
In languages that distinguish between methods and properties, properties (or getters) are often preferred for singletons:
1234567891011121314151617181920212223242526272829303132
// Method style - traditionalclass ConfigurationManagerA { private static instance: ConfigurationManagerA | null = null; public static getInstance(): ConfigurationManagerA { if (!this.instance) { this.instance = new ConfigurationManagerA(); } return this.instance; }}// Usage: ConfigurationManagerA.getInstance() // Property style - modern, cleanerclass ConfigurationManagerB { private static _instance: ConfigurationManagerB | null = null; public static get instance(): ConfigurationManagerB { if (!this._instance) { this._instance = new ConfigurationManagerB(); } return this._instance; }}// Usage: ConfigurationManagerB.instance // The difference is subtle but impacts readability at call sites:const dataA = ConfigurationManagerA.getInstance().get("key");const dataB = ConfigurationManagerB.instance.get("key"); // Properties feel more "natural" - the singleton IS the instance,// not something you actively "get"The accessor method embodies the pattern's promise: 'Give me access to THE instance, creating it if necessary.' The method should be idempotent—calling it any number of times returns the same instance. Some implementations throw exceptions if initialization fails; others return null. Be explicit about your contract.
The private constructor isn't merely a convention—it's the enforcement mechanism that makes Singleton work. Without it, the pattern is just a suggestion that well-meaning developers might ignore.
What privatization prevents:
super() to create their own instances (the constructor can be protected for extension points)12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// WITHOUT private constructor - the pattern can be violated class BrokenSingleton { private static instance: BrokenSingleton | null = null; // Public constructor - anyone can call it! constructor() { console.log("BrokenSingleton: Constructor called"); } public static getInstance(): BrokenSingleton { if (!BrokenSingleton.instance) { BrokenSingleton.instance = new BrokenSingleton(); } return BrokenSingleton.instance; }} // The pattern's intent is violated:const singleton = BrokenSingleton.getInstance();const rogue1 = new BrokenSingleton(); // Works! Creates second instance!const rogue2 = new BrokenSingleton(); // Works! Creates third instance! console.log(singleton === rogue1); // false - multiple instances exist! // WITH private constructor - violation is impossible class ProperSingleton { private static instance: ProperSingleton | null = null; // Private constructor - only the class can call it private constructor() { console.log("ProperSingleton: Constructor called"); } public static getInstance(): ProperSingleton { if (!ProperSingleton.instance) { ProperSingleton.instance = new ProperSingleton(); } return ProperSingleton.instance; }} const properSingleton = ProperSingleton.getInstance();// const rogue = new ProperSingleton(); // Compile error!// Error: Constructor of class 'ProperSingleton' is privateThe private constructor provides compile-time protection in statically-typed languages. In dynamically-typed languages like Python or JavaScript, the protection is weaker—developers can still bypass it. In these languages, the singleton pattern relies more on convention and the accessor method design than constructor access control.
We've established the classic Singleton implementation with its core mechanisms:
The basic Singleton implementation works correctly in single-threaded environments. But real-world applications are concurrent. The next page explores Singleton implementation challenges—particularly the thread-safety issues that make naive lazy initialization dangerous in multi-threaded contexts.