Singleton Pattern explained simply
Thilan Dissanayaka Software Architecture January 27, 2020

Singleton Pattern explained simply

Ever needed just one instance of a class in your application? Maybe a logger, a database connection, or a configuration manager? This is where the Singleton Pattern comes in — one of the simplest but most powerful design patterns in software engineering.

This pattern involves a single class which is responsible to create an object while making sure that only single object gets created. This class provides a way to access its only object which can be accessed directly without need to instantiate the object of the class.

What is the Singleton Pattern?

At its core, the Singleton Pattern ensures that:

  • Only one instance of a class is ever created.
  • It provides a global access point to that instance.
  • You control the lifecycle of the object — no more unnecessary setups.

Here is the UML class diagram that shows how the pattern is structured. A Client never calls new Singleton() directly — it always goes through getInstance().

<mxfile>
  <diagram name="Singleton Pattern">
    <mxGraphModel dx="1024" dy="768" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="800" pageHeight="400">
      <root>
        <mxCell id="0"/>
        <mxCell id="1" parent="0"/>
        <!-- Singleton class box -->
        <mxCell id="2" value="&lt;b&gt;Singleton&lt;/b&gt;&#xa;——————————————&#xa;- instance: Singleton (static)&#xa;——————————————&#xa;- Singleton()&#xa;+ getInstance(): Singleton (static)" style="shape=rectangle;whiteSpace=wrap;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;fontSize=13;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
          <mxGeometry x="400" y="80" width="280" height="120" as="geometry"/>
        </mxCell>
        <!-- Client box -->
        <mxCell id="3" value="&lt;b&gt;Client&lt;/b&gt;" style="shape=rectangle;whiteSpace=wrap;align=center;verticalAlign=middle;fontSize=13;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
          <mxGeometry x="80" y="110" width="140" height="60" as="geometry"/>
        </mxCell>
        <!-- Arrow from Client to Singleton -->
        <mxCell id="4" value="getInstance()" style="endArrow=open;endSize=12;strokeWidth=1.5;fontSize=12;" edge="1" source="3" target="2" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
      </root>
    </mxGraphModel>
  </diagram>
</mxfile>

A Quick Example (Without Singleton)

Take this simple Logger class:

public class Logger {
    public Logger() {
        // Perform setup operations
        System.out.println(\"Logger setup operations performed.\");
    }
    
    public void log(String message) {
        // Log the provided message
        System.out.println(\"Logging: \" + message);
    }
}

public class MainProgram {
    public static Logger MyGlobalLogger = new Logger();
    
    public static void main(String[] args) {
        System.out.println(\"Main program started.\");
        
        // Some code here...
        
        // We might not need to log anything, but the logger setup still occurs.
        
        // If we need to log something:
        MyGlobalLogger.log(\"This is a log message.\");
    }
}

Here’s the problem — even if we never log a message, the logger still gets created. If the setup is heavy, that is wasted resources for nothing. Not ideal.

Enter Singleton Pattern

With Singleton, we create the object only when needed, and only once.

Here’s how:

public class Logger {
    private static Logger instance;
    
    // Private constructor to prevent instantiation from outside
    private Logger() {
        // Perform setup operations
        System.out.println(\"Logger setup operations performed.\");
    }
    
    // Static method to get the singleton instance
    public static Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }
        return instance;
    }
    
    public void log(String message) {
        // Log the provided message
        System.out.println(\"Logging: \" + message);
    }
}

Next we can use this in our main program. Unlike in previous code we don’t create an object. Access the logger through the getInstance() method

public static Logger MyGlobalLogger = Logger.getInstance();

MyGlobalLogger.log(\"This is a log message.\");

What if we try to create an object like this?

public static Logger MyGlobalLogger = new Logger();

This will cause a compilation error because the constructor is private and cannot be accessed from outside the Logger class.

Why use lazy instantiation?

  • Objects are only created when it is needed
  • Helps control that we’ve created the Singleton just once.
  • If it is resource intensive to set up, we want to do it once.

Real-World Example: Database Connection Pool

So guys, the Logger example above is clean and simple, but let’s look at something you will actually run into on the job. In any web application, you need a database connection pool. Creating a new pool for every request would be insane — you would exhaust database connections in seconds under load. You want exactly one pool, shared across all your request handlers.

Let’s get into it.

public class DatabaseConnectionPool {
    private static volatile DatabaseConnectionPool instance;
    private final List<Connection> connectionPool;
    private static final int POOL_SIZE = 10;

    private DatabaseConnectionPool(String dbUrl, String user, String password) {
        connectionPool = new ArrayList<>(POOL_SIZE);
        for (int i = 0; i < POOL_SIZE; i++) {
            try {
                connectionPool.add(DriverManager.getConnection(dbUrl, user, password));
            } catch (SQLException e) {
                throw new RuntimeException("Failed to initialize connection pool", e);
            }
        }
        System.out.println("Connection pool created with " + POOL_SIZE + " connections.");
    }

    public static DatabaseConnectionPool getInstance(String dbUrl, String user, String password) {
        if (instance == null) {
            synchronized (DatabaseConnectionPool.class) {
                if (instance == null) {
                    instance = new DatabaseConnectionPool(dbUrl, user, password);
                }
            }
        }
        return instance;
    }

    public synchronized Connection getConnection() {
        if (connectionPool.isEmpty()) {
            throw new RuntimeException("No available connections in the pool.");
        }
        return connectionPool.remove(connectionPool.size() - 1);
    }

    public synchronized void releaseConnection(Connection connection) {
        connectionPool.add(connection);
    }
}

What’s happening here? The private constructor opens a fixed number of database connections up front. After that, every part of your application — your user service, your order handler, your auth middleware — all call DatabaseConnectionPool.getInstance() and get back the same pool. No duplicate pools, no wasted connections.

Here is how you would use it across different request handlers:

// In your user service
DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(
    "jdbc:mysql://localhost:3306/mydb", "admin", "secret"
);
Connection conn = pool.getConnection();
// run your query...
pool.releaseConnection(conn);

// In your order service — same pool, same connections
DatabaseConnectionPool samePool = DatabaseConnectionPool.getInstance(
    "jdbc:mysql://localhost:3306/mydb", "admin", "secret"
);
// samePool == pool — true, it is the exact same object

This is the Singleton pattern doing real work. One pool, many consumers, zero waste.

Handling Multi-Threaded Access

What would happen if two different threads accessed this line at the same time?

public static Singleton getInstance()
{
    if (instance == null) {
        instance = new Logger();
    }
}

In a multi-threaded environment, two threads could simultaneously pass the if (instance == null) check and create multiple instances. This breaks the Singleton pattern.

Synchronized Method (Thread-Safe but Slow)

public class Logger {
    private static Logger instance;

    private Logger() {
        System.out.println(\"Logger setup operations performed.\");
    }

    public static Logger getInstance() {
        synchronized (Logger.class) {
            if (instance == null) {
                instance = new Logger();
            }
        }
        return instance;
    }

    public void log(String message) {
        System.out.println(\"Logging: \" + message);
    }
}

This ensures thread safety but synchronizing the entire method may degrade performance.

Double-Checked Locking (Thread-Safe and Efficient)

public class Logger {
    private static volatile Logger instance;

    private Logger() {
        System.out.println(\"Logger setup operations performed.\");
    }

    public static Logger getInstance() {
        if (instance == null) {
            synchronized (Logger.class) {
                if (instance == null) {
                    instance = new Logger();
                }
            }
        }
        return instance;
    }

    public void log(String message) {
        System.out.println(\"Logging: \" + message);
    }
}
  • Volatile Keyword: Ensures visibility of changes to variables across threads.
  • Double Check: Reduces the need to synchronize after the object is created.

Local Variable Optimization: Reduces the need to access the volatile variable multiple times, improving performance.

public class Logger {
    private static volatile Logger instance;

    private Logger() {
        System.out.println(\"Logger setup operations performed.\");
    }

    public static Logger getInstance() {
        Logger result = instance;
        if (result == null) {
            synchronized (Logger.class) {
                result = instance;
                if (result == null) {
                    result = new Logger();
                }
            }
        }
        return result;
    }

    public void log(String message) {
        System.out.println(\"Logging: \" + message);
    }
}

Use Cases for Singleton Pattern

  • Logging: Ensure consistent logging across the application using a single logger instance.

  • Database Connection: Manage a single connection pool object for handling database interactions.

  • Caching: Store frequently accessed data in memory for quick retrieval.

  • Configuration Management: Centralize application-wide configuration settings.

  • Thread Pools: Maintain a single thread pool instance to control and reuse worker threads.

  • Device Drivers: Ensure a single access point for hardware resources like printers or sensors.

  • Authentication Manager: Manage user sessions and authentication checks consistently.

  • Service Locator: Provide access to a globally available service without repeated instantiation.

The Singleton pattern is useful when you need a single, shared object to manage global state or resource-intensive operations. Among the different approaches, Bill Pugh Singleton Design is the most efficient and widely recommended approach for modern Java applications.

ALSO READ
Blockchain 0x000 – Understanding the Fundamentals
May 21, 2020 Web3 Development

Imagine a world where strangers can exchange money, share data, or execute agreements without ever needing to trust a central authority. No banks, no intermediaries, no single point of failure yet...

Identity and Access Management (IAM)
May 11, 2020 Identity & Access Management

Who are you — and what are you allowed to do? That's the fundamental question every secure system must answer. And it's exactly what Identity and Access Management (IAM) is built to solve.

How I built a web based CPU Simulator
May 07, 2020 Pet Projects

As someone passionate about computer engineering, reverse engineering, and system internals, I've always been fascinated by what happens "under the hood" of a computer. This curiosity led me to...

Writing a Shell Code for Linux
Apr 21, 2020 Exploit Development

Shellcode is a small piece of machine code used as the payload in exploit development. In this post, we write Linux shellcode from scratch — starting with a simple exit, building up to spawning a shell, and explaining every decision along the way.

Exploiting a Stack Buffer Overflow on Windows
Apr 12, 2020 Exploit Development

In a previous tutorial we discusses how we can exploit a buffer overflow vulnerability on a Linux machine. I wen through all theories in depth and explained each step. Now today we are going to jump...

Access Control Models
Apr 08, 2020 Identity & Access Management

Access control is one of the most fundamental concepts in security. Every time you set file permissions, assign user roles, or restrict access to a resource, you're implementing some form of access control. But not all access control is created equal...

Exploiting a  Stack Buffer Overflow  on Linux
Apr 01, 2020 Exploit Development

Have you ever wondered how attackers gain control over remote servers? How do they just run some exploit and compromise a computer? If we dive into the actual context, there is no magic happening....

Basic concepts of Cryptography
Mar 01, 2020 Cryptography

Ever notice that little padlock icon in your browser's address bar? That's cryptography working silently in the background, protecting everything you do online. Whether you're sending an email,...

Common Web Application Attacks
Feb 05, 2020 Application Security

Web applications are one of the most targeted surfaces by attackers. This is primarily because they are accessible over the internet, making them exposed and potentially vulnerable. Since these...

Remote Code Execution (RCE)
Jan 02, 2020 Application Security

Remote Code Execution (RCE) is the holy grail of application security vulnerabilities. It allows an attacker to execute arbitrary code on a remote server — and the consequences are as bad as it sounds. In this post, we'll go deep into RCE across multiple languages, including PHP, Java, Python, and Node.js.