DEV Community

Syed Yakoob Shiraz
Syed Yakoob Shiraz

Posted on

Understanding the Singleton Pattern – One Instance to Rule Them All

📘 Why This Blog?

When I first encountered the Singleton Design Pattern, I thought, "Why do we need this? Can’t we just use a static class?"
It felt like extra ceremony for something simple—until I built a multithreaded logging utility and saw chaos unfold.

That’s when the Singleton Pattern clicked.

This devblog is my honest take on helping you understand the Singleton Pattern—the way I wish someone had explained it to me.


📖 What is the Singleton Pattern?

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

In simple terms: You create an object once and reuse it everywhere.


🔍 Spotting a Singleton Pattern

You might be looking at a Singleton Pattern if you see:

  • A class with a private constructor
  • A static method like getInstance() or getLogger()
  • A static field that holds a single instance
  • Code like: Logger logger = Logger.getInstance();

🧪 Code Example – Logger Singleton

Step 1: Trivial Singleton (Not Thread-Safe)

public class Logger {
    private static Logger instance;

    private Logger() {
        System.out.println("Logger initialized!");
    }

    public static Logger getInstance() {
        if (instance == null) {
            instance = new Logger(); // ⚠ Not thread-safe!
        }
        return instance;
    }

    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
}
Enter fullscreen mode Exit fullscreen mode

⚠ Drawback: This implementation is not safe in multithreaded environments. If two threads call getInstance() at the same time when instance is null, they might both create separate instances—breaking the singleton guarantee.


Step 2: Thread-Safe Singleton (Synchronized)

public class Logger {
    private static Logger instance;

    private Logger() {}

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

    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
}
Enter fullscreen mode Exit fullscreen mode

⚠ Drawback: While this approach is thread-safe, the synchronized keyword makes the method slower because every call to getInstance() acquires a lock—even after the instance is initialized. This can cause performance bottlenecks in high-concurrency applications.


Step 3: Optimized Singleton (Double-Checked Locking)

public class Logger {
    private static volatile Logger instance;

    private Logger() {}

    public static Logger getInstance() {
        if (instance == null) {
            synchronized (Logger.class) {
                if (instance == null) {
                    instance = new Logger(); // ✅ Thread-safe and efficient
                }
            }
        }
        return instance;
    }

    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Use the Singleton in Your Code

public class Main {
    public static void main(String[] args) {
        // Creating multiple threads to simulate race condition
        Runnable task = () -> {
            Logger logger = Logger.getInstance();
            logger.log("Logging from thread: " + Thread.currentThread().getName());
        };

        Thread thread1 = new Thread(task, "Thread-1");
        Thread thread2 = new Thread(task, "Thread-2");

        thread1.start();
        thread2.start();
    }
}
Enter fullscreen mode Exit fullscreen mode

⚠ Warning: If you're using the trivial (non-thread-safe) Singleton, running this code may lead to two separate instances being created, violating the Singleton principle. This showcases why thread safety is crucial in multi-threaded environments.


🧠 Personal Notes to Apply the Singleton Pattern

  • Always make the constructor private to prevent direct instantiation.
  • Use volatile for the instance when applying double-checked locking.
  • Use synchronized wisely—only when needed.
  • Singleton is a global resource—use it for shared services like logging, configuration, or caching.
  • Avoid abusing it as a workaround for dependency injection.

✅ Benefits

  • Guarantees a single instance of a class
  • Saves memory for expensive or shared resources
  • Ensures consistent state across the application
  • Great for logging, configuration managers, or connection pools
  • Easily accessible from anywhere in your codebase

❗ Caution

  • Overusing Singleton can lead to hidden dependencies and tight coupling
  • Makes unit testing harder due to global state
  • Avoid using it for data classes or business logic that needs multiple instances

🔗 More Examples

👉 Singleton Design Pattern on GitHub


🧵 TL;DR

The Singleton Pattern is your go-to when you need just one object—and you want it safely shared across the system.

Start simple, go thread-safe, and optimize with double-checked locking if performance matters.

One instance to rule them all.

Made with ❤️ by syedyshiraz

Top comments (0)