Singleton Design Pattern in Java: The Definitive Guide (All Implementations + Pitfalls)

Your application needs exactly one database connection pool. Or one configuration registry. Or one thread-managing scheduler. Creating two by accident would be a bug β€” yet nothing in Java’s language rules stops you from calling new ConnectionPool() a hundred times. The Singleton pattern solves this by making the class itself responsible for ensuring only one instance ever exists.

This guide builds every major Singleton implementation from the simplest to the most robust, explaining exactly what breaks in each naΓ―ve version so the next implementation makes sense. By the end you will know which variant to use by default, which to avoid, and how Singleton fits into modern Spring applications.

Tested with Java 21. All examples compile and run with no external dependencies.

The Problem: Shared Resources That Must Not Be Duplicated

Consider a class that manages a pool of database connections. Opening a connection is expensive β€” it involves network handshakes, authentication, and memory allocation. In a real application, you manage a fixed pool (say, 10 connections) and hand them out to callers as needed.

What happens when two different parts of the application each do new ConnectionPool()? You get two independent pools. Threads using Pool A have no idea Pool B exists. You might exhaust the database’s allowed connections without any single pool knowing it, and none of the pool statistics make sense. This is the classic reason for Singleton: one logical resource should be backed by one Java object.

We will use a simple AppConfig class as our example throughout β€” a configuration holder that reads a file once at startup and caches the values. Creating it twice would waste I/O, cause inconsistency if the file changes between reads, and waste memory.

What Is the Singleton Pattern?

The Singleton is a creational design pattern. It ensures a class has only one instance, and provides a global point of access to it.

“Ensure a class only has one instance, and provide a global point of access to it.” β€” Design Patterns, Gamma et al.

Two things together: exactly one instance (enforced by the class itself, not by convention) and global access (via a static method, typically getInstance()). The implementations below differ in when the instance is created and how they handle concurrent creation attempts.

Implementation 1: Eager Initialization β€” Simplest Possible

The simplest Singleton creates the instance when the class is loaded by the JVM, before any thread can call getInstance(). This is called eager initialization because it does not wait to see if the instance is actually needed.

public class AppConfig {

    // The JVM creates this instance when the class is first loaded.
    // static guarantees it belongs to the class, not any instance.
    // final guarantees it can never be reassigned.
    private static final AppConfig INSTANCE = new AppConfig();

    // Private constructor: prevents any other class from calling new AppConfig()
    private AppConfig() {
        System.out.println("Reading configuration from file...");
        // load properties, set up values, etc.
    }

    // The only way to get the instance
    public static AppConfig getInstance() {
        return INSTANCE;
    }

    public String getProperty(String key) {
        return System.getProperty(key, "(not set)");
    }
}

// Usage:
AppConfig config1 = AppConfig.getInstance();
AppConfig config2 = AppConfig.getInstance();
System.out.println(config1 == config2); // true β€” same object

Three rules enforced here: the private constructor blocks outside construction; the static final field ensures one instance at class-load time; and getInstance() is the only public path to the object. Because the JVM guarantees class initialization happens exactly once per classloader, this is inherently thread-safe with zero synchronization overhead.

The trade-off: the instance is created even if your application never calls getInstance(). For lightweight objects like a config reader this is fine. For a heavy resource (a full connection pool, an expensive computation) you pay the cost at startup whether you need it or not. That leads to the next implementation.

Implementation 2: NaΓ―ve Lazy Initialization β€” Do Not Use in Multithreaded Code

Lazy initialization creates the instance only when it is first requested. The intent is good β€” delay the expensive creation until someone actually needs it. Here is the naΓ―ve version:

public class AppConfig {

    private static AppConfig instance; // null until first request

    private AppConfig() { /* load config */ }

    // BROKEN IN MULTITHREADED CODE β€” see explanation below
    public static AppConfig getInstance() {
        if (instance == null) {            // Thread A: checks, sees null
            instance = new AppConfig();    // Thread B: also sees null, also enters here
        }                                  // Both threads create a new AppConfig!
        return instance;
    }
}

The problem is a race condition at the if (instance == null) check. Thread A checks, sees null, and is about to call new AppConfig(). Before it finishes, Thread B also checks, also sees null (Thread A’s assignment has not happened yet), and also calls new AppConfig(). Now you have two instances β€” exactly what Singleton is supposed to prevent. One thread returns one instance; the other thread returns a different instance. Chaos.

⚠️ Watch Out: This naΓ―ve version works correctly in single-threaded unit tests β€” which is exactly why developers miss the bug in production. Always assume your application is multithreaded unless you have an explicit contract that it is not.

Implementation 3: Synchronized Method β€” Safe but Slow

The straightforward fix: synchronize the entire getInstance() method so only one thread can enter it at a time.

public class AppConfig {

    private static AppConfig instance;

    private AppConfig() { /* load config */ }

    // synchronized: only one thread can execute this method at a time
    public static synchronized AppConfig getInstance() {
        if (instance == null) {
            instance = new AppConfig(); // safe β€” no other thread can be here
        }
        return instance;
    }
}

This is now thread-safe. But synchronized acquires a lock on every call to getInstance() β€” even after the instance has already been created and every call is just returning the existing object. In a high-traffic application where thousands of threads call getInstance() per second, those unnecessary lock acquisitions create a serious bottleneck. The lock is only needed during the one-time initialization, not for every subsequent lookup.

Implementation 4: Double-Checked Locking β€” Safe and Fast

Double-Checked Locking (DCL) solves the performance problem by checking null twice β€” once without the lock (fast path for the common case), and once inside the lock (safe path for first initialization). But it requires one critical keyword to work correctly: volatile.

public class AppConfig {

    // volatile is MANDATORY for DCL to work correctly.
    // Without it, the JVM can reorder the writes inside the constructor
    // and return a partially-constructed object to a second thread.
    private static volatile AppConfig instance;

    private AppConfig() { /* load config */ }

    public static AppConfig getInstance() {

        // First check: no lock, instant return for the 99.99% case
        // (instance already created, just return it)
        if (instance == null) {

            // Lock only when first check says we might need to create
            synchronized (AppConfig.class) {

                // Second check: re-verify under the lock.
                // Another thread might have created the instance between
                // our first check and acquiring the lock.
                if (instance == null) {
                    instance = new AppConfig();
                }
            }
        }
        return instance;
    }
}

Why is volatile mandatory? Without it, the JVM is allowed to reorder memory operations for performance. The line instance = new AppConfig() is actually three steps: allocate memory, initialize the object, assign the reference. Without volatile, the JVM may do steps 1 and 3 before step 2 β€” meaning a second thread sees a non-null reference but reads from an incompletely initialized object. volatile creates a memory barrier that prevents this reordering. This was the critical flaw in pre-Java 5 DCL; it was fixed by the Java Memory Model update in Java 5 (JSR 133) and works correctly with volatile from Java 5 onward.

Implementation 5: Initialization-on-Demand Holder β€” The Recommended Approach

The Holder idiom (also called Initialization-on-Demand Holder, or IODH) is the cleanest thread-safe lazy Singleton in Java. It uses a well-known JVM guarantee β€” that inner classes are not loaded until they are first referenced β€” to achieve lazy initialization with zero synchronization overhead and zero volatile fields.

public class AppConfig {

    private AppConfig() { /* load config */ }

    // The JVM loads Holder only when getInstance() is first called.
    // Class loading is inherently thread-safe β€” the JVM ensures it happens once.
    // No synchronized, no volatile, no locks β€” just the JVM class loading guarantee.
    private static class Holder {
        static final AppConfig INSTANCE = new AppConfig();
    }

    public static AppConfig getInstance() {
        // This reference triggers Holder to load (if it hasn't already).
        // After first call, Holder is already loaded and this is just a field read.
        return Holder.INSTANCE;
    }
}

Why does this work without any explicit synchronization? The JVM specification (JLS Β§12.4) guarantees that a class is initialized exactly once, and that if multiple threads race to trigger initialization, the JVM serializes them internally. Holder is not loaded when AppConfig is loaded β€” it is only loaded when Holder.INSTANCE is first accessed. That happens inside getInstance(). First call: JVM loads Holder, runs the static initializer once, done. Every subsequent call: Holder is already loaded, JVM returns INSTANCE with a plain field read. No overhead at all on the hot path.

βœ… Best Practice: Use the Holder idiom as your default Singleton implementation. It is lazy, thread-safe, readable, and requires no synchronization keywords. Reach for Enum Singleton (next section) when you additionally need serialization safety.

Implementation 6: Enum Singleton β€” Bulletproof for Serialization

Joshua Bloch recommends the Enum Singleton in Effective Java as the most concise and robust implementation. A Java enum guarantees exactly one instance per named constant β€” a property enforced by the JVM itself, not by any code you write.

public enum AppConfig {

    INSTANCE; // The JVM creates this exactly once, period.

    // Enum instances can have fields and methods like any class
    private final String configPath;

    AppConfig() {
        // Enum constructors run once, when the constant is first used
        this.configPath = System.getProperty("config.path", "application.properties");
        System.out.println("Loading config from: " + configPath);
    }

    public String getProperty(String key) {
        return System.getProperty(key, "(not set)");
    }
}

// Usage:
AppConfig config = AppConfig.INSTANCE;
config.getProperty("db.url");

Enum Singleton handles two threats that every other implementation leaves open. First, serialization: if your Singleton implements Serializable, deserialization normally creates a new object β€” breaking the “one instance” guarantee. Enum values survive serialization correctly by default; the JVM maps deserialized enum constants back to the existing instance. Second, reflection: Constructor.setAccessible(true) can bypass a private constructor in class-based Singletons. The JVM actively prevents this for enum constructors and throws an exception if you try.

The limitation: enums cannot extend another class (they implicitly extend java.lang.Enum). If your Singleton needs to subclass something, use the Holder idiom instead.

Comparison: Which Implementation to Choose?

ImplementationThread-safe?Lazy?Serialization-safe?Reflection-safe?Use when
Eagerβœ… Yes❌ No❌ No*❌ No*Lightweight object, always needed, simplest code
NaΓ―ve lazy❌ Noβœ… Yes❌ No*❌ No*Never β€” only safe in single-threaded tests
Synchronized methodβœ… Yesβœ… Yes❌ No*❌ No*Rarely β€” only if throughput of getInstance() is irrelevant
DCL + volatileβœ… Yesβœ… Yes❌ No*❌ No*Java 5+ when you need lazy and want explicit synchronization control
Holder idiomβœ… Yesβœ… Yes❌ No*❌ No*Default choice. Clean, zero overhead, readable.
Enumβœ… Yesβœ… Yes**βœ… Yesβœ… YesWhen you need serialization or reflection safety; no superclass needed

* Serialization and reflection safety can be added manually to class-based Singletons (via readResolve() and access checks), but they require extra boilerplate. Enum handles both for free.
** Enum is lazy in the sense that the enum class is loaded on first use, similar to the Holder idiom.

Advanced: Breaking Singletons β€” and How to Defend Against It

Class-based Singletons (all except Enum) can be broken in two ways that are important to understand, even if you rarely encounter them in practice.

Breaking via Reflection

// An attacker can bypass the private constructor using reflection
Constructor<AppConfig> constructor = AppConfig.class.getDeclaredConstructor();
constructor.setAccessible(true);         // bypasses 'private'
AppConfig second = constructor.newInstance(); // new instance created!

// Defense: throw from the constructor if an instance already exists
private AppConfig() {
    if (Holder.INSTANCE != null) {
        throw new IllegalStateException("Use AppConfig.getInstance()");
    }
    // ... init ...
}

Breaking via Serialization

// If AppConfig implements Serializable, deserializing creates a second instance.
// Defense: implement readResolve() to return the existing instance.
public class AppConfig implements Serializable {

    // ... Holder idiom as before ...

    // Called by ObjectInputStream during deserialization.
    // Returning the existing instance replaces the deserialized one.
    protected Object readResolve() {
        return getInstance();
    }
}

Both defenses add boilerplate. If you need both, the Enum Singleton eliminates them entirely β€” it is the right tool for that scenario.

Singleton in Spring: GoF vs. Container-Managed

If you use Spring, you rarely need to implement the GoF Singleton pattern manually. Spring beans are singletons by default within the application context β€” the framework ensures that getBean(AppConfig.class) always returns the same object, without any static state in your class.

// Spring singleton: no private constructor, no getInstance(), no static field
@Component
public class AppConfig {

    @Value("${db.url}")
    private String dbUrl;

    public String getDbUrl() { return dbUrl; }
}

// Spring creates one AppConfig bean and injects it everywhere it is needed.
// You never call 'new AppConfig()'. Spring manages the lifecycle.
@Service
public class UserService {
    private final AppConfig config;

    public UserService(AppConfig config) {  // Spring injects the single instance
        this.config = config;
    }
}

The key difference: the GoF Singleton uses static state to enforce one instance globally across all classloaders. A Spring singleton is one instance per application context. In tests, you can start a fresh application context with a fresh bean β€” but the GoF static instance persists for the entire JVM lifetime. This makes GoF Singletons harder to test and harder to isolate. In Spring applications, prefer @Component + constructor injection over manual Singleton implementations.

πŸ’‘ Key Insight: Use the GoF Singleton pattern when you are writing code without a DI container (utilities, libraries, CLI tools). Use Spring’s @Component scope when you are inside a Spring application. The pattern solves the same problem; the mechanism differs.

When to Use Singleton β€” and When to Avoid It

Reach for Singleton when: A resource is expensive to create and must be shared (connection pools, thread pools, caches). A configuration object is read-only after initialization and used application-wide. A registry or registry-like service coordinates other objects and must see the full picture.

Avoid it when: You are inside a Spring (or Jakarta EE) application β€” let the container manage scope instead. The “single instance” is really just a coding convention, not a correctness requirement β€” a plain class with constructor injection is easier to test. You need to vary behavior per-environment or per-tenant β€” static state makes this very hard. Your singleton holds mutable state that multiple threads write to β€” you will need careful synchronization on every access, and a different design may be cleaner.

Two Pitfalls to Avoid

Pitfall 1: Mutable Static State Breaks Tests

A Singleton’s static instance survives the entire JVM lifetime. If your singleton holds mutable state (a cache, a counter, a list), state set in one test leaks into the next test. Test isolation breaks. The fix: make your singleton’s state immutable (read-only after construction), or use Spring’s application context instead of static state, so each test can get a fresh instance.

Pitfall 2: Singletons Are Global Variables With Extra Steps

The GoF book itself warns that “it can be difficult to tell whether a Singleton is appropriate.” The pattern solves a real problem (resource duplication), but it is often misused as a global variable store. When every class calls AppConfig.getInstance() instead of receiving a config via constructor injection, the dependency is hidden. Code becomes hard to reason about and hard to test. The cure: treat your Singleton as a library-level tool and inject it via constructors at the application layer, keeping dependencies explicit.

Frequently Asked Questions

Is Singleton thread-safe?

It depends on the implementation. Eager initialization and the Holder idiom are inherently thread-safe because they rely on class loading guarantees. NaΓ―ve lazy initialization is not. Synchronized method and DCL + volatile are thread-safe with different performance profiles. See the comparison table above.

Which Singleton implementation should I use by default?

The Holder idiom for most cases β€” it is lazy, thread-safe without locks, and readable. Use Enum Singleton when serialization or reflection safety is a real requirement. Use eager initialization when the object is always needed and lightweight enough that deferred creation adds no value.

Why did DCL not work before Java 5?

Java’s original memory model did not guarantee the ordering of writes within a constructor as seen by other threads. A thread could see the instance field as non-null before the constructor had finished writing all its fields. Java 5’s revised memory model (JSR 133) defined volatile precisely enough to prevent this reordering, making DCL safe when the field is declared volatile.

Can a Singleton have multiple instances in a JVM?

Yes β€” if multiple classloaders are used. Each classloader maintains its own version of every class, including the static fields. In an application server with separate classloaders per deployment unit, two deployed applications can each have their own Singleton instance. This is typically desirable (isolation), but if true JVM-wide uniqueness is required, the singleton must be loaded by the bootstrap or system classloader.

5 AI Prompts for Singleton

  1. “I have a class with a static getInstance() method but no volatile or synchronized keyword. Is this thread-safe? Show me what race condition can occur and how to fix it using the Holder idiom. [paste class]”
  2. “Refactor this GoF Singleton to a Spring @Component bean. Show the before class with private constructor and getInstance(), and the after version with constructor injection at the call site.”
  3. “Write a JUnit 5 test that verifies a Singleton class returns the same instance across 100 concurrent threads. Use an ExecutorService and a CountDownLatch to maximize concurrency at the getInstance() call.”
  4. “Explain why adding ‘implements Serializable’ to a Holder-idiom Singleton breaks the single-instance guarantee and show the readResolve() fix.”
  5. “I need an in-process event bus that must be the same object throughout the application. Should I use a GoF Singleton or a Spring @Component with @EventListener? Walk through the trade-offs for a Spring Boot 3 application.”

Conclusion

The Singleton pattern progresses logically: start with eager initialization (trivially correct, no lazy loading), observe the lazy initialization problem (race condition), fix it with synchronized (correct but slow), optimize with DCL + volatile (correct and fast), then reach the cleanest solution β€” the Holder idiom β€” which achieves all goals without any synchronization keywords. Add Enum when serialization safety matters.

In Spring applications, lean on the container instead. But understanding the GoF Singleton β€” and its failure modes β€” makes you a better engineer whether you reach for it or decide something else fits better.

See Also

Further Reading

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.