One App, One Instance: The Singleton Pattern Made Easy✌️

One App, One Instance: The Singleton Pattern Made Easy✌️

Imagine you are in a meeting. Ten people are in the room, and suddenly, everyone starts shouting different orders at the same time. No one knows who to listen to, work gets doubled, and the whole project becomes a mess.

To fix this, we usually pick one leader. Only that person gives the final orders.

In coding, we have the same problem. Sometimes, having too many "bosses" (objects) causes bugs. We solve this problem using the Singleton Pattern.

Why do we need a single shared instance?

Think about a logging system. Its job is to record what the application is doing, often writing logs to a common destination like a file or a centralized system.

Now imagine if every part of the application creates its own Logger object:

  • Concurrency issues: Multiple logger instances may write inconsistently or without proper synchronization, leading to interleaved or hard to debug logs.
  • Resource inefficiency: Creating multiple logger instances can lead to unnecessary memory usage and duplicated configurations.
  • Inconsistent behavior: Different instances might have different configurations (log levels, formats), making logs harder to analyze.

To avoid these problems, we use the Singleton pattern to ensure a single, shared logger instance, providing a centralized and consistent way to handle logging across the application.

The Evolution of the Singleton Pattern

To implement a Singleton, we hide the constructor (to prevent direct instantiation) and provide a global access point. Let’s look at how we can try to solve this in Java

1. The Eager Initialization

class EagerSingleton {
    private static final EagerSingleton instance = new EagerSingleton();
    
    private EagerSingleton() {}
    
    public static EagerSingleton getInstance() {
        return instance;
    }
}        

The Downside: If this object is memory heavy and your application never actually ends up using it, you have just wasted precious system memory for nothing.

2. The Lazy Initialization

class LazySingleton {
    private static LazySingleton instance;
    
    private LazySingleton() {}
    
    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}        

The Downside: It is not thread safe. If two different threads enter the if check at the exact same time, both will create a new object.

3. The Synchronized Method

A synchronized block allows only one thread at a time to enter and execute

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

The Downside: Even after the instance is initialized, every call to getInstance() still requires acquiring a lock. This unnecessary synchronization introduces performance overhead since locking is only needed during the initial creation phase.

4. Double Checked Locking

class DoubleCheckedSingleton {
    private static volatile DoubleCheckedSingleton instance;
    
    private DoubleCheckedSingleton() {}
    
    public static DoubleCheckedSingleton getInstance() {
        if (instance == null) { // Check 1
            synchronized (DoubleCheckedSingleton.class) {
                if (instance == null) { // Check 2
                    instance = new DoubleCheckedSingleton();
                }
            }
        }
        return instance;
    }
}        

The Downside: While highly performant and thread safe, it is complex to write. Furthermore, It can still be broken using mechanisms like reflection or improper serialization handling.

5. The Enum Singleton

If you want a method that is incredibly simple yet solves all the problems mentioned above, look no further than the Java Enum.

public enum EnumSingleton {
    INSTANCE;
    
    public void executeLogging(String message) {
        System.out.println("Log: " + message);
    }
}        

Why this reigns supreme:

  1. Absolute Simplicity: No complex double checks or synchronized blocks.
  2. Inherently Thread Safe: Java guarantees that enum values are instantiated in a thread safe manner.
  3. Ironclad Reflection & Serialization Defense: Java internally protects enums from being duplicated via reflection or serialization. It is practically bulletproof.

How this Works Internally:

A. Class Loading

The JVM loads the EnumSingleton class into memory. Because INSTANCE is a static field, it is initialized during the class loading phase.

B. Thread Safety by Default

The JVM guarantees that a class is only loaded once. It also ensures that all static initialization is finished before any thread can use the class.

The Result: If ten threads try to access INSTANCE at the exact same time for the first time, the JVM makes them wait until the one and only INSTANCE is fully created. This is why you don't need synchronized blocks with Enums.

Why is it "Bulletproof"?

The reason the Enum approach is better than the "Double Checked Locking" method is how it handles two specific "attacks":

No "Reflection" Attacks

Normally, a clever programmer can use Reflection to change a private constructor to public and create a second instance of a Singleton. However, Java’s Constructor.newInstance() method has a specific check. If it sees you are trying to reflectively create an enum, it throws an IllegalArgumentException. The "front door" is physically welded shut.

Built-in Serialization Safety

When we serialize (save) a Java object to a file and later deserialize (load) it back, Java typically creates a new object instance in memory. This behavior can break the Singleton pattern because it results in multiple instances of the same class.

However, Enums handle this differently. Instead of saving the entire object state, Java only stores the name of the enum constant (for example, "INSTANCE"). During deserialization, the JVM does not create a new object. Instead, it looks up the existing enum constant by name and returns the same instance. This ensures that even after serialization and deserialization, the Singleton property is preserved.


That’s all I had to share. Hope you found this insightful and learned something new about the Singleton pattern!😊

Let’s Talk!

Which of these ways do you use in your own projects? Do you prefer the Double Check method or the simple Enum? Let me know in the comments! 👇

I wrote this article as part of my learning journey! If I missed anything or if you have advice on how to improve this, please let me know in the comments. I’d love to hear about other ways to build Singletons too!

Singleton is a great starting point for LLD. The key interview follow-up is always thread safety - double-checked locking vs enum singleton in Java, and why lazy initialization matters when the singleton holds expensive resources like DB connection pools.

If the Singleton pattern ensures one instance per JVM, does that mean it quietly breaks in a microservices world by design 😅 ? Asking because I wonder if we're teaching a pattern that's already architecturally obsolete.

Learned something new 🙂↕️ Looking forward to your next post

To view or add a comment, sign in

Others also viewed

Explore content categories