Singleton Pattern

Introduction

The Singleton pattern ensures a class has only one instance and provides a global point of access to it. It is one of the most widely used — and misused — design patterns in software engineering. When applied correctly, it solves real problems around shared resource management, such as configuration settings, connection pools, and logging systems.

The Problem

Imagine your application needs a centralized configuration manager that reads settings from a file or environment variables. If every part of your codebase creates its own instance, you end up with duplicated I/O, inconsistent state, and wasted memory. Worse, if one module changes a setting in its copy, other modules never see the update. You need a single, shared source of truth.

The Solution

The Singleton pattern solves this by restricting instantiation to a single object. The class itself controls its own creation, stores the instance privately, and exposes it through a static access method. Every caller gets the same instance, guaranteeing consistent state across the entire application.

Key Principle

Controlled access to a sole instance. The class owns its lifecycle — no external code can create or destroy additional instances. This gives you a single coordination point without relying on global variables.

Java Example

This example implements a thread-safe ConfigurationManager that loads application settings once and provides them everywhere.

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class ConfigurationManager {

    // volatile ensures visibility across threads
    private static volatile ConfigurationManager instance;

    private final Properties properties;

    // Private constructor prevents external instantiation
    private ConfigurationManager() {
        properties = new Properties();
        try (FileInputStream fis = new FileInputStream("app.properties")) {
            properties.load(fis);
            System.out.println("Configuration loaded from app.properties");
        } catch (IOException e) {
            System.err.println("Failed to load config: " + e.getMessage());
            // Load defaults
            properties.setProperty("db.host", "localhost");
            properties.setProperty("db.port", "5432");
            properties.setProperty("app.name", "MyApplication");
        }
    }

    // Double-checked locking for thread safety
    public static ConfigurationManager getInstance() {
        if (instance == null) {
            synchronized (ConfigurationManager.class) {
                if (instance == null) {
                    instance = new ConfigurationManager();
                }
            }
        }
        return instance;
    }

    public String get(String key) {
        return properties.getProperty(key);
    }

    public String get(String key, String defaultValue) {
        return properties.getProperty(key, defaultValue);
    }

    public int getInt(String key, int defaultValue) {
        String value = properties.getProperty(key);
        if (value == null) return defaultValue;
        try {
            return Integer.parseInt(value.trim());
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }
}

// Usage
public class Application {
    public static void main(String[] args) {
        ConfigurationManager config = ConfigurationManager.getInstance();

        String dbHost = config.get("db.host", "localhost");
        int dbPort = config.getInt("db.port", 5432);
        String appName = config.get("app.name", "DefaultApp");

        System.out.println("App: " + appName);
        System.out.println("DB: " + dbHost + ":" + dbPort);

        // Same instance everywhere
        ConfigurationManager sameConfig = ConfigurationManager.getInstance();
        System.out.println("Same instance: " + (config == sameConfig)); // true
    }
}

Python Example

The same configuration manager scenario in Python, using a class-level instance with thread safety.

import threading
import json
import os


class ConfigurationManager:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                # Double-checked locking
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance._initialized = False
        return cls._instance

    def __init__(self):
        if self._initialized:
            return
        self._initialized = True
        self._config = {}
        self._load_config()

    def _load_config(self):
        config_path = "config.json"
        if os.path.exists(config_path):
            with open(config_path, "r") as f:
                self._config = json.load(f)
                print("Configuration loaded from config.json")
        else:
            # Load defaults
            self._config = {
                "db.host": "localhost",
                "db.port": 5432,
                "app.name": "MyApplication",
            }
            print("Loaded default configuration")

    def get(self, key, default=None):
        return self._config.get(key, default)

    def get_int(self, key, default=0):
        value = self._config.get(key)
        if value is None:
            return default
        try:
            return int(value)
        except (ValueError, TypeError):
            return default


# Usage
if __name__ == "__main__":
    config = ConfigurationManager()

    db_host = config.get("db.host", "localhost")
    db_port = config.get_int("db.port", 5432)
    app_name = config.get("app.name", "DefaultApp")

    print(f"App: {app_name}")
    print(f"DB: {db_host}:{db_port}")

    # Same instance everywhere
    same_config = ConfigurationManager()
    print(f"Same instance: {config is same_config}")  # True

When to Use

  • Configuration management — when your application needs a single source of truth for settings loaded from files, environment variables, or remote config services.
  • Connection pooling — managing a shared pool of database or HTTP connections where multiple instances would waste resources or cause connection limits to be exceeded.
  • Logging — a centralized logger that all modules write to, ensuring consistent log formatting, output destinations, and thread-safe writes.
  • Caching layers — an in-memory cache (like an LRU cache) that must be shared across the application to avoid duplicate data and stale entries.

Real-World Usage

  • Java RuntimeRuntime.getRuntime() returns the single Runtime instance for the JVM.
  • Spring Framework — Spring beans are singletons by default. The IoC container manages one instance per bean definition.
  • Python logging modulelogging.getLogger(name) returns the same logger instance for a given name across the entire application.
  • Android SharedPreferences — the preferences instance is cached and shared across components to avoid redundant disk reads.
  • Database drivers — connection managers in libraries like HikariCP and SQLAlchemy use singleton-like patterns to manage connection pools.



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *