CLOSE
Updated on 30 Jun, 202652 mins read 1,491 views

Imagine you are building a large enterprise application.

You create:

class DatabaseConnection
{
};

A developer creates:

DatabaseConnection db1;

Another developer creates:

DatabaseConnection db2;

Another:

DatabaseConnection db3;

Soon:

100 Database Connections

exist.

Problems begin:

Connection Pool Exhausted
Memory Waste
Resource Contention
Inconsistent State

But the application only needed:

ONE Database Manager

The problem appears repeatedly:

  • Logging systems
  • Configuration managers
  • Cache managers
  • Connection pools
  • Application settings

The solution became Singleton Pattern

Historical Background

One of the earliest recurring software problems was:

How Do We Guarantee
Only One Instance Exists?

Developers often used:

global variables

Example:

DatabaseConnection globalDB;

This worked.

But created problems:

Uncontrolled Access
Initialization Issues
Testing Problems
Namespace Pollution

Singleton attempted to provide:

Global Access + Controlled Creation

Intent of Singleton

Official intent:

Ensure a class has only one instance and provide a global point of access to it.

Notice:

Single solves TWO problems:

Problem 1:

Exactly one instance

Problem 2:

Global access

Most discussions focus only one:

One Instance

But:

Global Access

is equally important.

What is the Singleton Design Pattern?

The Singleton Pattern ensures that a class has only one instance and provides a global point of access to it.

It is commonly used for:

  • Configuration managers
  • Logging systems
  • Database connections
  • Thread pools

In simpler terms:

Imagine you are building an application where you only want one shared object throughout the lifecycle of the program. This is where Singleton comes into play – it restricts object creation and guarantees that all parts of your application use the same object.

The Problem It solves

In a typical application, creating multiple objects of a class might not be problematic. However, in certain scenarios – like logging, configuration handling, or managing a database connection – you want just one instance to avoid redundancy, excessive memory use, or inconsistent behavior.

When to use Singleton Method Design Pattern?

Use the Singleton method Design Pattern when:

  1. There must be exactly one instance of a class and it must be accessible to clients from a well-known access point:
    1. This requirement encapsulates the essence of the Singleton pattern. It ensures that only one instance of the class exists, providing a global point of access to it. For example, in a game application, there might be a need for a single instance of a GameManager class to manage game states and resources.
  2. When the sole instance should be extensible by subclassing and clients should be able to use an extended instance without modifying:
    1. This implies that the Singleton instance should support inheritance, allowing clients to use extended versions without altering their code. This requirement can be fulfilled by designing the Singleton class with protected or virtual methods that subclasses can override. For instance, in a GUI framework, a WindowManager Singleton class might be extended to support additional window types or behaviors.
  3. Singleton classes are used for logging, driver objects, caching, thread pool, and database connections:
    1. These are classic examples where the Singleton pattern proves its worth.
      1. Logging: A Logger Singleton class ensures that all parts of the application log messages using the same logger instance, maintaining consistency in log formatting and destination.
      2. Driver Objects: For hardware interaction, a Singleton representing the hardware driver ensures that there's only one instance managing device communication.
      3. Caching: A CacheManager Singleton provides a central point for caching frequently used data, optimizing performance by reducing redundant computations.
      4. Thread Pool: A ThreadPool Singleton manages a pool of worker threads, ensuring efficient utilization of system resources in multi-threaded applications.
      5. Database Connections: A DatabaseConnection Singleton manages database connections, ensuring that all database operations share the same connection, reducing overhead and ensuring transaction consistency.

In all these cases, the Singleton pattern ensures that there's only one instance of the class, providing a centralized point of access for clients while managing global resources efficiently. It promotes code maintainability, scalability, and flexibility, making it a valuable design pattern in various software engineering scenarios.

Implementation in C++

#include <iostream>

class Singleton {
private:
    // Private constructor to prevent instantiation
    Singleton() {}

    // Static instance of the class
    static Singleton* instance;

public:
    // Method to access the Singleton instance
    static Singleton* getInstance() {
        // Lazy initialization: create instance only when needed
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }

    // Example method to demonstrate functionality
    void showMessage() {
        std::cout << "Hello, I am a Singleton instance!" << std::endl;
    }
};

// Initializing static member instance
Singleton* Singleton::instance = nullptr;

int main() {
    // Accessing Singleton instance
    Singleton* singletonInstance = Singleton::getInstance();
    singletonInstance->showMessage();

    return 0;
}

Key Points:

Private Constructor:

The constructor of the Singleton class is private, preventing the instantiation of the class from outside.

Prevents:

Singleton singleton;

from compiling.

Only:

getInstance()

can create the object.

Therefore:

Controlled Instantiation

Visual Flow

Application

      |
      v

getInstance()

      |
      v

instance exists?

   /      \
 No        Yes
 |          |
Create    Return
Object    Existing
  1. Static Instance:
    1. A static member variable instance of type Singleton* holds the sole instance of the class.
  2. getInstance() Method:
    1. The getInstance() method provides a static point of access to the Singleton instance.
    2. It follows the lazy initialization approach, creating the instance only when it's first accessed.
  3. Usage:
    1. Clients can access the Singleton instance using Singleton::getInstance().

Why Is It a Creational Pattern?

The Singleton Pattern falls under the creational design patterns. This is because it deals with how objects are created. Unlike simple instantiation (new), Singleton controls the object creation process by returning an existing instance rather than creating a new one.

Identifying the Need for a Singleton

Imagine you are developing a loggin service. You need a class that writes logs to a file. If every part of your application creates a new logger instance, the result might be:

  • Overwritten logs
  • Multiple file handles
  • Synchronization issues

Instead, if there's only one logger instance (a Singleton_, all parts of the program write to the same log file in a controlled manner.

Working of Singleton Pattern

The Singleton Pattern typically involves the following steps:

  1. Private Constructor
    1. Prevents external code from using new to create objects.
    2. Only the class itself can construct the object.
  2. Static Instance Variable
    1. Holds the single instance of the class.
    2. Shared across all uses of the class.
  3. Public Static Method (getInstance())
    1. Checks if the instance exists.
    2. If not, creates it.
    3. Always returns the same instance.

Real-Life Analogy

Imagine a remote control for your air conditioner.

There is only one remote, and everyone uses it to interact with the AC.

That's the Singleton pattern – one shared instance used by all.

Approaches to Implement Singleton Pattern

In the real world, while designing the product, there are two primary ways to implement the Singleton pattern.

  • Eager Loading
  • Lazy Loading

Each with its own trade-offs in terms of performance, memory usage, and thread safety.

1️⃣ Eager Loading (Early Initialization)

In Eager Loading, the Singleton instance is created as soon as the class is loaded, regardless of whether it's ever used. Let's understand this with a real-life analogy.

  • Object is created at the start of the program.

Real-World Analogy:

Fire Extinguisher in a Building
A fire extinguisher is always present, even if a fire never occurs. Similarly, eager loading creates the Singleton instance upfront, just in case it's needed.

Example Code:

class Singleton {
private:
    static Singleton* instance;
    Singleton() {}

public:
    static Singleton* getInstance() {
        return instance;
    }
};
Singleton* Singleton::instance = new Singleton();

Understanding:

  • The object is created immediately when the class is loaded.
  • It's always available and inherently thread-safe.

Pros:

  • Simple to implement.
  • thread-safe without any extra handling.

Cons:

  • Wastes memory if the instance is never used.
  • Not suitable for heavy objects.

2️⃣ Lazy Loading (On-Demand Initialization)

In Lazy Loading, the Singleton instance is created only when it's needed – the first time the getInstance() method is called.

  • Object is created only when it's needed.

Real-World Analogy:

Coffee Machine
Imagine a coffee machine that only brews coffee when you press the button. It doesn't waste energy or resources until you actually want a cup. Similarly, lazy loading creates the Singleton instance only when it's requested.

Example Code:

class Singleton {
private:
    static Singleton* instance;
    Singleton() {}

public:
    static Singleton* getInstance() {
        if (!instance)
            instance = new Singleton();
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;

Understanding:

  • The instance starts as null
  • It is only created when getInstance() is the first called.
  • Future calls returns the already created instance.

Pros:

  • Saves memory if the instance is never used.
  • Object creation is deferred until required

Cons:

  • Lazy Loading is Not thread-safe by default. Thus, it requires synchronization in multi-threaded environment.

Thread Safety: A Critical Concern in Singleton Pattern

In a single-threaded environment, implementing a Singleton is straightforward. However, things get complicated in multi-threaded application, which are very common in modern software (especially web servers, mobile apps, etc.).

The Problem:

Let's say two threads simultaneously call getInstance() for the first time in a lazy-loaded Singleton. If the instance has not been created yet, both threads might pass the null check and end up creating two different instances – completely breaking the Singleton guarantee.

This kind of bugs is:

  • Hard to detect, as it may not occur every time.
  • Severe, because it defeats the whole purpose of the pattern.
  • Costly, especially if the Singleton manages critical resources like logging, configuration, or DB connections.

Consider:

Thread A:

Singleton::getInstance();

Thread B:

Singleton::getInstance();

Both execute simultaneously.

Both see:

instance == nullptr

Both create objects.

Result:

Two Singletons

Requirement violated.

This is one of the most common Singleton bugs.

Different Ways to Achieve Thread Safety

There are several ways to make the Singleton pattern thread-safe. Here are a few common approaches:

1️⃣ Synchronized Method

This is the simplest way to ensure thread safety. By synchronizing the method that creates the instance, we can prevent multiple threads from creating separate instances at the same time. However, this approach can lead to performance issues due to the overhead of synchronization.

Consider the following code snippet for better understanding:

#include <mutex>

class Singleton {
private:
    static Singleton* instance;
    static std::mutex mtx;
    Singleton() {}

public:
    static Singleton* getInstance() {
        std::lock_guard<std::mutex> lock(mtx);
        if (!instance)
            instance = new Singleton();
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

2️⃣ Double-Checked Locking

Reduces overhead by locking only during the first creation.

class Singleton {
private:
    static Singleton* instance;
    static std::mutex mtx;
    Singleton() {}

public:
    static Singleton* getInstance() {
        if (!instance) {
            std::lock_guard<std::mutex> lock(mtx);
            if (!instance)
                instance = new Singleton();
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

3️⃣ Meyers Singleton (Modern C++)

The preferred implementation.

class Singleton
{

public:
	static Singleton& getInstance()
	{
		static Singleton instance;
		
		return instance;
	}

private:
	Singletong() = default;
};

Usage:

Singleton& singleton = Singleton::getInstance();

Advantages:

Simple
Lazy
Thread Safe
Efficient

Since:

C++11

local static initialization is thread-safe.

Preventing Copies

Potential issue:

Singleton copy = Singleton::getInstance();

Creates another instance.

Fix:

class Singleton
{
public:
	Singleton(const Singleton&) = delete;

	Singleto& operator=(const Singleton&) = delete;
};

Now copying is prohibited.

Complete Modern Singleton:

class Singleton
{
private:

    Singleton() = default;

public:

    static Singleton&
    getInstance()
    {
        static Singleton instance;

        return instance;
    }

    Singleton(
        const Singleton&
    ) = delete;

    Singleton&
    operator=(
        const Singleton&
    ) = delete;
};

UML Structure

+-------------------+
|     Singleton     |
+-------------------+
| - constructor()   |
| - instance        |
+-------------------+
| + getInstance()   |
+-------------------+

Only one access point exists.

Benefits of Singleton

Controlled Instance Count

Guarantees one object.

Global Access

Accessible everywhere.

Lazy Creation

Created when needed

Resource Management

Usefull for expensive resources.

Consistent State

Single source of truth.

The Hidden Problem

Singleton is often described as:

An Improved Global Variable

Notice:

Singleton::getInstance()

can be called:

Anywhere

Meaning:

Global State Still Exists

This leads to problems.

Testing Problems

Consider:

OrderService

uses:

Logger:getInstance()

directly.

Unit test:

Cannot Replace Logger

Test becomes harder.

Mocking becomes difficult.

Hidden Dependencies

Bad:

class OrderService
{
};

Internally:

Logger::getInstance()

Dependency invisible.

Reader sees:

No Logger Dependency

But one exists.

Architectural smell.

Singleton and SOLID

DIP Violation instead of ILogger dependency, code directly accesses:

Logger singleton

Tight coupling.

Enterprise Perspective

Historically:

Singletons were extremely popular.

Today:

Many enterprise systems avoid them.

Instead Use:

Dependency Injection
Service Containers
Application Contexts

Why?

Because:

Global State
Does Not Scale Well

Example: Better Alternative

Instead of:

Logger::getInstance()

Inject:

ILogger&

Example:

class OrderService
{
private:

    ILogger& logger;

public:

    OrderService(
        ILogger& logger
    )
        : logger(logger)
    {
    }
};

Benefits:

Testable
Flexible
Loosely Coupled

When Singleton Is Appropriate

Use Singleton when:

Truly One Instance Exists

Examples:

Configuration
Application Metadata
System Clock Wrapper

Shared Resource

Examples:

Connection Pool
Cache Manager

When Singleton Is Not Appropriate

Avoid when:

Business Logic Depends On It

Testing Is Important

Multiple Instances May Be Needed Later

Dependency Injection Exists

In modern architectures:

Many Singleton use cases disappear.

 

Buy Me A Coffee

Leave a comment

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

Your experience on this site will be improved by allowing cookies Cookie Policy