Skip to main content

Command Palette

Search for a command to run...

Singleton Design Pattern

Updated
5 min read
Singleton Design Pattern

The Problem

In many applications, certain resources should exist only once throughout the application's lifecycle.

Consider a configuration manager. If different parts of the application create their own configuration objects, each instance may load the same configuration file repeatedly, consume additional memory, and potentially hold inconsistent state.

Similarly, components such as loggers, cache managers, and feature flag managers often need to be shared across the entire application.

The challenge is simple:

How do we ensure that only one instance of a class exists while still providing global access to it?

This is the problem that the Singleton Design Pattern solves.

What is a Singleton?

The Singleton Design Pattern ensures that:

  • Only one instance of a class exists.

  • The instance is globally accessible.

  • The class controls its own object creation.

A class implementing this pattern is called a Singleton class.

Typical use cases include:

  • Configuration Manager

  • Logger

  • Cache Manager

  • Feature Flag Manager

  • Metrics Collector

1. Eager Initialization

In this approach, the instance is created when the class is loaded.

// Thread-safe because class loading is handled by JVM
// Disadvantage : Instance is created even if it is never used and May waste memory for heavyweight objects
class Singleton {

    private static final Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}
// This approach works well when the object is lightweight and guaranteed to be required.

2. Lazy Initialization

In Lazy Initialization, the instance is created only when it is needed for the first time.

// Object is created only when required and Saves memory if object is never used
// Only disadvantage is - Not thread-safe
class Singleton {

    private static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {

        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }
}
/*
Consider two threads executing simultaneously:

Thread A -> instance == null
Thread B -> instance == null

Both threads may create separate objects, breaking the Singleton guarantee.
*/

3. Synchronized getInstance()

To make Lazy Initialization thread-safe, we can synchronize the method.

// Disadvantages :
// Performance overhead
// Every call acquires a lock, even after the instance has already been created
// For applications with frequent access to the Singleton, this can become a bottleneck
class Singleton {

    private static Singleton instance = null;

    private Singleton() {}

    public static synchronized Singleton getInstance() {

        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }
}

4. Double Checked Locking (DCL)

Double Checked Locking reduces unnecessary synchronization.

class Singleton {

    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {

        if (instance == null) {

            synchronized (Singleton.class) {

                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }
}
/*

Why is volatile Required?

Many developers memorize the use of volatile without understanding the reason.

The statement:

instance = new Singleton();

is not an atomic operation.

Internally, it can be broken into three steps:

1. Allocate memory
2. Initialize object (run constructor)
3. Assign reference to instance

Without volatile, the JVM or CPU may reorder operations:

1. Allocate memory
3. Assign reference
2. Initialize object


Now another thread may observe:

instance != null

and start using an object that has not been fully initialized.

The volatile keyword prevents this reordering and guarantees visibility across threads.
*/

6. Enum Singleton

Java provides an even simpler approach using enums.

// Benefits:
//  - JVM guarantees exactly one instance per enum constant
//  - Thread-safe by default (class loading is thread-safe)
//  - Serialization-safe: JVM prevents creating new instances during deserialization
//  - Reflection-safe: enum constructors cannot be called via reflection
//  - Free from the volatile/DCL complexity
//
// Limitation: Cannot extend a class
public enum Singleton {

    INSTANCE;

    private int count = 0;

    public void incrementCounter() {
        count++;
        System.out.println(count);
    }

    public int getCounter() {
        return count;
    }
}

Usage:

Singleton.INSTANCE.incrementCounter();

For most Java applications, Enum Singleton is considered the most robust implementation.

Singleton Does Not Mean One Instance in the Entire System

A common misconception is:

Singleton means one object in the entire application ecosystem.

This is incorrect.

Singleton guarantees:

One instance per JVM process.

Consider three application servers:

Server A -> Singleton Instance
Server B -> Singleton Instance
Server C -> Singleton Instance

Even though each server uses Singleton, there are still three separate instances.

This distinction becomes important in distributed systems.

If a truly single coordinator is required across multiple machines, technologies such as distributed locks, leader election, or coordination systems are needed.

When Should You Avoid Singleton?

Singleton introduces global state.

Overusing it can create several problems:

  • Tight coupling

  • Hidden dependencies

  • Difficult unit testing

  • Harder maintenance

For business logic and domain services, dependency injection is often a better choice.

For example:

UserService(Logger logger)

is generally easier to test and maintain than:

Logger.getInstance()

everywhere in the codebase.

The Singleton pattern solves a specific problem: controlling the creation of a shared object and ensuring that only one instance exists within a JVM.

The implementation itself is straightforward. The real challenge is understanding the tradeoffs. While Singleton can simplify access to shared infrastructure such as loggers and configuration managers, excessive use can introduce global state and hidden dependencies.

Like most design patterns, Singleton is neither good nor bad by itself. Its effectiveness depends on where and why it is used.