Singleton Design Pattern in Java: The Complete Senior Developer Guide (2026)

The Singleton design pattern sounds simple — “one instance, one class.” But if an interviewer gives you five minutes and a whiteboard, the real depth reveals itself fast. Thread safety, memory model guarantees, reflection attacks, serialization loopholes, classloader edge cases, and how Spring quietly replaces the whole thing.

This guide covers everything a senior Java developer needs: all six implementations, their trade-offs, every known way to break the pattern, how to defend against each, real-world usage, and 15 interview questions with detailed answers.

singleton-design-pattern-java

Table of Contents

What Is the Singleton Pattern?

The Singleton pattern is a creational design pattern that:

  • Ensures a class has exactly one instance throughout the application’s lifetime
  • Provides a global access point to that instance

It belongs to the “Gang of Four” (GoF) design patterns, introduced in the 1994 book Design Patterns: Elements of Reusable Object-Oriented Software.

The Three Pillars of Any Singleton

  1. Private constructor — prevents external instantiation via new
  2. Private static instance variable — holds the single instance
  3. Public static getInstance() method — the controlled access point
public class Singleton {
    private static Singleton instance;

    private Singleton() {}  // No one outside can call new Singleton()

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

This is the simplest form — but it’s not thread-safe. Don’t use it in production. We’ll fix that below.

When Should You Use Singleton?

Use Singleton only when exactly one shared object must coordinate actions across the entire system. Classic signals:

  • The object is expensive to create (database connection pools, thread pools)
  • The object must maintain global state that all consumers read from the same source (configuration, logging)
  • Multiple instances would cause incorrect behaviour (a single cache that multiple services write to)

If you cannot articulate why multiple instances would cause a problem, you probably don’t need a Singleton.

Real-World Use Cases

1. Logger

The most universally recognised use case. A logging framework like java.util.logging.Logger or SLF4J maintains a single registry of loggers, ensuring output goes to the same target without duplication.

public class AppLogger {
    private static AppLogger instance;
    private final Logger logger = Logger.getLogger(AppLogger.class.getName());

    private AppLogger() {}

    public static AppLogger getInstance() {
        // (use Bill Pugh or DCL in production)
        if (instance == null) {
            instance = new AppLogger();
        }
        return instance;
    }

    public void log(String message) {
        logger.info(message);
    }
}

2. Application Configuration

Reading application.properties or environment variables once at startup, then serving them throughout the app.

public class AppConfig {
    private static AppConfig instance;
    private final Properties properties = new Properties();

    private AppConfig() {
        try (InputStream input = getClass().getClassLoader()
                .getResourceAsStream("application.properties")) {
            properties.load(input);
        } catch (IOException e) {
            throw new RuntimeException("Failed to load config", e);
        }
    }

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

    public String get(String key) {
        return properties.getProperty(key);
    }
}

3. Connection Pool Manager

A connection pool manager wraps the actual pool (like HikariCP). The manager itself is a Singleton; the connections it dispenses are short-lived.

4. Thread Pool (ExecutorService)

public class AppExecutor {
    private static AppExecutor instance;
    private final ExecutorService pool = Executors.newFixedThreadPool(10);

    private AppExecutor() {}

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

    public ExecutorService getPool() { return pool; }
}

5. Java JDK Singletons (Built-In Examples)

ClassHow accessed
java.lang.RuntimeRuntime.getRuntime()
java.awt.DesktopDesktop.getDesktop()
java.lang.SystemStatic methods only
java.util.logging.LogManagerLogManager.getLogManager()

6 Singleton Implementations in Java

1. Eager Initialization

The instance is created when the class is loaded by the JVM — even if it’s never used.

public class EagerSingleton {

    // Created at class-load time — thread-safe by JVM guarantee
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    private EagerSingleton() {}

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

Pros: Simple. Thread-safe by default (JVM guarantees class initialisation is atomic).
Cons: Instance created even if never used — wastes resources if initialisation is expensive or may throw exceptions you can’t catch cleanly.


2. Static Block Initialization

Same as eager, but lets you handle exceptions during initialisation.

public class StaticBlockSingleton {

    private static final StaticBlockSingleton INSTANCE;

    static {
        try {
            INSTANCE = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Failed to create singleton", e);
        }
    }

    private StaticBlockSingleton() {}

    public static StaticBlockSingleton getInstance() {
        return INSTANCE;
    }
}

Pros: Can handle checked exceptions during creation. Thread-safe.
Cons: Still eager — instance created at class load regardless of need.


3. Lazy Initialization (Not Thread-Safe)

Instance is created only when getInstance() is first called.

public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton() {}

    // ⚠️ NOT thread-safe — never use in multithreaded code
    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

Pros: Lazy — only created when needed.
Cons: Race condition — two threads can both see instance == null simultaneously and create two instances. Do not use in production.


4. Synchronized Method (Thread-Safe, Slow)

Adding synchronized makes the check atomic but synchronises the entire method on every call — even after the instance is created.

public class SynchronizedSingleton {

    private static SynchronizedSingleton instance;

    private SynchronizedSingleton() {}

    // Thread-safe but every call acquires a lock — high contention
    public static synchronized SynchronizedSingleton getInstance() {
        if (instance == null) {
            instance = new SynchronizedSingleton();
        }
        return instance;
    }
}

Pros: Correct. Lazy.
Cons: synchronized is only needed once (the first call). Every subsequent call still acquires the monitor lock — significant performance bottleneck under high load.


5. Double-Checked Locking (DCL)

The production standard for lazy, high-performance, thread-safe singleton. Synchronises only the critical first-time creation block.

public class DCLSingleton {

    // volatile is NON-NEGOTIABLE — without it, DCL is broken
    private static volatile DCLSingleton instance;

    private DCLSingleton() {}

    public static DCLSingleton getInstance() {
        if (instance == null) {                    // First check (no lock)
            synchronized (DCLSingleton.class) {
                if (instance == null) {            // Second check (with lock)
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

Why volatile is mandatory:
Without volatile, the JVM or CPU can reorder the steps of new DCLSingleton():

  1. Allocate memory
  2. Assign reference to instance ← reordering can put this before step 3
  3. Initialise object fields

A second thread can see a non-null instance reference that points to a partially initialised object. volatile enforces a happens-before relationship that prevents this reordering.

Pros: Lazy. Thread-safe. Lock only acquired once on the critical path.
Cons: More complex. Requires Java 5+ memory model (which all modern Java satisfies). Vulnerable to Reflection and Serialization attacks (see below).

The cleanest approach. Uses the JVM’s class loading guarantee — an inner static class is not loaded until it is referenced. No synchronized, no volatile needed.

public class BillPughSingleton {

    private BillPughSingleton() {}

    // Inner class is NOT loaded until getInstance() is called
    private static class SingletonHolder {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Why it’s thread-safe without synchronized:
The JVM guarantees that class initialisation is performed by a single thread while other threads wait. SingletonHolder.INSTANCE is assigned exactly once during class loading. No locks, no volatile, no race condition.

Pros: Lazy. Thread-safe. Simple. No synchronisation overhead.
Cons: Still vulnerable to Reflection and Serialization attacks.

Bonus: Enum Singleton (Anti-Reflection Champion)

Joshua Bloch’s Effective Java recommendation. The only implementation that is immune to Reflection attacks by JVM design.

public enum EnumSingleton {
    INSTANCE;

    // Add your singleton methods here
    public void connect() {
        System.out.println("Connected via EnumSingleton");
    }
}

// Usage
EnumSingleton.INSTANCE.connect();

Pros: Reflection-proof (JVM blocks Constructor.newInstance() on enums). Serialization-safe (enum serialisation preserves the single instance automatically). Thread-safe. Concise.
Cons: Cannot be lazily initialised. Cannot extend another class (enums implicitly extend java.lang.Enum). Feels unconventional to developers unfamiliar with the idiom.

ImplementationLazy?Thread-Safe?Reflection-Safe?Serialization-Safe?Recommended?
Eager✅ for simple cases
Static Block✅ if init can throw
Lazy (basic)❌ Never
Synchronized❌ Too slow
DCL + volatile✅ High-performance
Bill Pugh✅ Best general use
Enum✅ Best for pure singleton

Implementation Comparison Table

How to Break the Singleton — and How to Fix It

This section is what separates a candidate who “knows Singleton” from one who truly understands it.

Breaking with Reflection

Java Reflection can bypass the private constructor:

// Breaking Bill Pugh singleton with Reflection
BillPughSingleton instance1 = BillPughSingleton.getInstance();

Constructor<BillPughSingleton> constructor =
    BillPughSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);  // Bypass private access
BillPughSingleton instance2 = constructor.newInstance();

System.out.println(instance1 == instance2);  // false — broken!

Fix: Guard in the constructor

public class ReflectionSafeSingleton {

    private static volatile ReflectionSafeSingleton instance;

    private ReflectionSafeSingleton() {
        if (instance != null) {
            throw new IllegalStateException(
                "Singleton already initialised — use getInstance()");
        }
    }

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

Best fix: Use the Enum singleton — the JVM itself blocks Constructor.newInstance() on enum types, throwing IllegalArgumentException. No constructor guard needed.

Breaking with Serialization

When you serialise and then deserialise a Singleton, Java’s default serialisation creates a brand new object, bypassing the constructor entirely.

// Breaking with serialization (class must implement Serializable)
BillPughSingleton instance1 = BillPughSingleton.getInstance();

// Serialize
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
out.writeObject(instance1);
out.close();

// Deserialize
ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.ser"));
BillPughSingleton instance2 = (BillPughSingleton) in.readObject();
in.close();

System.out.println(instance1 == instance2);  // false — broken!

Fix: Implement readResolve()

public class SerializationSafeSingleton implements Serializable {

    private static final long serialVersionUID = 1L;

    private static class Holder {
        private static final SerializationSafeSingleton INSTANCE =
            new SerializationSafeSingleton();
    }

    private SerializationSafeSingleton() {}

    public static SerializationSafeSingleton getInstance() {
        return Holder.INSTANCE;
    }

    // JVM calls this after deserialisation — return existing instance
    protected Object readResolve() {
        return getInstance();
    }
}

Enum fix again: Enum serialisation is handled specially by Java — it serialises only the enum name and looks up the existing constant on deserialisation. No readResolve() needed.

Breaking with Cloning

If your Singleton class implements Cloneable, clone() will produce a second instance:

BillPughSingleton clone = (BillPughSingleton) instance1.clone();
System.out.println(instance1 == clone);  // false — broken!

Fix: Override clone() and throw an exception:

@Override
protected Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException("Cloning a Singleton is not allowed");
}

Breaking with Multiple ClassLoaders

In environments with multiple classloaders (OSGi containers, some application servers, custom plugin systems), each classloader can load the Singleton class independently — producing one instance per classloader.

Fix: Explicitly specify the classloader when loading the class, or use a classloader that sits above all others (e.g., the bootstrap or system classloader).

// Force loading via the system classloader
Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass(
    "com.gangforcode.MySingleton");

In most modern Spring/Jakarta EE apps this is handled by the container — not something you’ll fix manually, but something you must be aware of in an interview.

Thread Safety Deep Dive: The Java Memory Model

Understanding why volatile matters in DCL requires understanding the Java Memory Model (JMM).

The Problem Without volatile

Object creation (new DCLSingleton()) is not atomic. The JVM performs three steps:

  1. Allocate memory for the object
  2. Initialise the object’s fields (run the constructor)
  3. Assign the memory address to the instance reference

The JVM and CPU are permitted to reorder steps 2 and 3. This means a second thread can read a non-null instance reference that points to memory where the constructor hasn’t run yet — and proceed to use a partially constructed object.

How volatile Fixes It

volatile enforces a happens-before relationship. Any write to a volatile field is visible to all subsequent reads of that field. More importantly for DCL, it prevents the reordering of steps 2 and 3 — the constructor must complete before the reference is published.

The Two Questions to Ask When Reviewing Singleton Code

  1. Can two threads enter the constructor simultaneously? (mutual exclusion)
  2. Can one thread observe the reference before initialisation is complete? (safe publication)

If either answer is “maybe,” treat it as a production bug — even if it hasn’t triggered yet.

Singleton in Spring — What Changes?

Here is something most Singleton articles skip: in a Spring application, you almost never need to implement Singleton yourself.

Spring’s IoC container manages bean scopes. By default, every Spring bean is a Singleton scoped to the Spring ApplicationContext — created once, shared across all injection points.

@Service  // or @Component, @Repository, @Bean
public class MyService {
    // Spring instantiates this once and injects the same instance everywhere
}
@Configuration
public class AppConfig {
    @Bean
    @Scope("singleton")  // This is the default — you don't even need to write it
    public DataSource dataSource() {
        return DataSourceBuilder.create().url("jdbc:postgresql://...").build();
    }
}

Spring Singleton vs. Classic Singleton

Classic SingletonSpring Singleton
ScopeOne per JVMOne per ApplicationContext
EnforcementPrivate constructorContainer-managed
Thread safetyDeveloper’s responsibilityDeveloper’s responsibility
TestabilityHard (global state)Easy (inject mocks)
Multiple instances?Impossible by designPossible with multiple contexts

Senior insight: Spring Singleton scope is per-ApplicationContext, not per-JVM. In tests that create multiple contexts, you can have multiple “singletons.”

Prefer Dependency Injection Over Manual Singletons

In modern Java applications, if you find yourself writing getInstance(), ask whether Spring (or Guice, or Micronaut) can manage the lifecycle instead. DI containers give you:

  • Easier unit testing (inject mocks instead of global state)
  • Clear dependency graph
  • Lifecycle management (startup, shutdown hooks)

Singleton vs Static Class

A common interview question: “When do you use Singleton over a class with all static methods?”

SingletonStatic Class
InstantiationOne instanceNever instantiated
InheritanceCan implement interfaces, extend classesCannot
StateCan hold mutable instance stateOnly static fields
Lazy initPossibleNot applicable
TestabilityCan be mockedHard to mock/replace
PolymorphismSupportedNot supported
SerializationSupportedNot supported

Rule of thumb: Use a static class for stateless utility methods (Math, Arrays, Collections). Use Singleton when you need state, polymorphism, or lifecycle control.

Singleton and Virtual Threads (Java 21+)

Java 21 introduced Virtual Threads (Project Loom) as a stable feature. If your Singleton is used heavily under virtual thread workloads, there are two things to know:

1. synchronized Blocks and Pinning

In Java 21, a virtual thread “mounts” on a platform (OS) thread. If a virtual thread executes a synchronized block and blocks inside it, it pins the platform thread — defeating the purpose of virtual threads.

The synchronized getInstance() method (approach #4 above) can cause pinning. In high-throughput virtual-thread applications, prefer Bill Pugh (no synchronisation at all on the hot path) or Enum Singleton.

Java 23+ resolves this with improvements to synchronized and virtual thread pinning, but the safest designs avoid synchronisation on the hot path entirely.

2. Thread-Local State

If your Singleton uses ThreadLocal for per-thread state, virtual threads change the calculus — there may be millions of virtual threads, and a ThreadLocal value per virtual thread means millions of values. Design carefully.

When NOT to Use Singleton

Despite its popularity, Singleton is also one of the most misused patterns. Avoid it when:

  • The object doesn’t actually need to be shared — just pass it as a constructor argument
  • You need to test the class — Singletons with global state make unit testing painful (you can’t reset state between tests easily)
  • The object has heavy mutable shared state — global mutable state is a concurrency hazard
  • You’re in a distributed system — “one instance” means one per JVM. Across microservices or cluster nodes, you need distributed coordination (Redis, ZooKeeper), not a Singleton
  • You’re using Spring — let the container manage lifecycle instead

15 Senior-Level Interview Questions and Answers

Q1. What is the Singleton design pattern and what problem does it solve?

A: Singleton ensures a class has exactly one instance throughout the application’s lifetime and provides a global access point to it. It solves the problem of coordinating shared resources — like a configuration store, connection pool, or logger — where multiple instances would cause inconsistency, duplication, or resource waste.

Q2. Why is the basic lazy Singleton not thread-safe?

A: Two threads can simultaneously evaluate instance == null as true before either has created the instance. Both then proceed to call the constructor, producing two instances — violating the Singleton contract. The fix is synchronisation (DCL or Bill Pugh).

Q3. Why does DCL require volatile on the instance variable?

A: Without volatile, the JVM or CPU can reorder the steps of object construction: it may assign the reference to instance before the constructor has finished executing. A second thread seeing a non-null instance would skip the synchronised block and use a partially initialised object. volatile enforces a happens-before guarantee, preventing this reordering.

Q4. What is the Bill Pugh Singleton and why is it preferred over DCL?

A: The Bill Pugh Singleton uses a private static inner class (SingletonHolder) that is not loaded until getInstance() is first called. The JVM guarantees that class initialisation is atomic and performed by a single thread. This provides lazy initialisation and thread safety with no synchronized keyword, no volatile, and no lock contention on the hot path.

Q5. How does Reflection break the Singleton pattern and how do you prevent it?

A: Constructor.setAccessible(true) bypasses Java’s access control, allowing constructor.newInstance() on the private constructor to create a second instance. Prevention: throw IllegalStateException inside the constructor if an instance already exists. Best prevention: use an Enum Singleton — the JVM itself blocks Constructor.newInstance() on enum types.

Q6. How does serialisation break Singleton and what is readResolve()?

A: Java’s default deserialisation creates a new object from the serialised byte stream, bypassing the private constructor. The fix is to implement readResolve() — a special method the JVM calls after deserialisation instead of returning the new object. readResolve() returns getInstance(), preserving the single instance. Enum Singletons handle this automatically.

Q7. Which Singleton implementation is immune to both Reflection and Serialization attacks?

A: The Enum Singleton. The JVM guarantees that enum constants are initialised exactly once, blocks reflective constructor invocation, and handles serialisation by name — restoring the existing constant rather than creating a new object.

Q8. What happens to Singleton in a multi-classloader environment?

A: Each classloader maintains its own namespace. If two classloaders load the same Singleton class, each gets its own class object and therefore its own instance — creating multiple “singletons.” This occurs in OSGi containers, some application servers, and custom plugin systems. The fix is to ensure the Singleton class is loaded by a common parent classloader, or to use a classloader-aware registry.

Q9. What is the difference between Singleton in classic Java and Singleton in Spring?

A: A classic Singleton is enforced by the class itself (private constructor, one per JVM). A Spring Singleton is one per ApplicationContext — enforced by the container, not the class. Spring beans can have multiple instances if multiple contexts exist (common in tests). Spring Singletons also support interface-based proxying, AOP, and lifecycle callbacks that classic Singletons cannot.

Q10. When would you choose Singleton over a static utility class?

A: Choose Singleton when you need: mutable instance state, the ability to implement interfaces or extend a class, polymorphic behaviour, testability via mocking, lazy initialisation, or serialisation support. Use a static class only for stateless utility methods (Math, StringUtils) where none of those concerns apply.

Q11. Can a Singleton be subclassed?

A: Not meaningfully. The private constructor prevents subclassing from outside the class. You could technically declare a protected or package-private constructor, but then the subclass can create its own instances — violating the Singleton contract. The design doesn’t support inheritance.

Q12. How do you write a unit test for a class that depends on a Singleton?

A: This is one of Singleton’s main weaknesses. Approaches:

  1. Refactor to DI: Pass the dependency as a constructor argument instead of calling getInstance(). Then inject a mock in tests.
  2. Reflection: Reset the static instance field to null between tests using Field.setAccessible(true).
  3. Use an interface: Have the Singleton implement an interface, inject the interface — mock the interface in tests.

In well-designed Spring applications, you inject beans by interface and never call getInstance(), making mocking trivial.

Q13. How does Singleton interact with virtual threads in Java 21?

A: If your Singleton uses a synchronized method on the access path (approach #4), virtual threads executing that method may pin the underlying platform thread while blocked — harming throughput. The Bill Pugh and Enum implementations avoid synchronisation on the hot path entirely and are safe for virtual-thread workloads. Singleton that uses ThreadLocal should be audited — with millions of virtual threads, per-thread storage can become a memory concern.

Q14. How does Cloning break Singleton and how do you prevent it?

A: If the Singleton class implements Cloneable (or inherits clone() from a superclass without overriding it), object.clone() creates a shallow copy — a second instance. Prevention: override clone() and throw CloneNotSupportedException.

Q15. Is Singleton a good pattern? When would you argue against it?

A: Singleton solves a real problem — controlled shared access to a resource. But it’s often overused. Arguments against:

  • Global state is dangerous — it creates hidden coupling between classes and makes behaviour harder to reason about
  • Testing is painful — global state persists between tests unless carefully reset
  • Distributed systems — Singleton means one per JVM, not one per cluster; Redis/ZooKeeper handle the distributed case
  • Spring makes it unnecessary — in modern Java, the container should manage lifecycle, not the class itself

In a senior interview, acknowledging these trade-offs shows maturity. The pattern is not inherently bad — it’s that it’s frequently applied where DI would be better.

Summary and Best Practice Cheatsheet

ScenarioRecommended Implementation
Simple, stateless singletonEager initialisation
Lazy, high-performance, no reflection concernsBill Pugh (inner static holder)
Lazy + high-throughput multithreadedDCL with volatile
Maximum safety (reflection + serialization proof)Enum Singleton
Spring applicationJust use @Service / @Component — let Spring manage it
Needs to implement an interfaceBill Pugh or DCL

The Five Rules to Live By

  1. Use volatile in DCL — always, no exceptions
  2. Prefer Bill Pugh over synchronized methods — no lock on the hot path
  3. Use Enum when you need reflection and serialisation safety with zero effort
  4. Implement readResolve() if your Singleton is Serializable and you’re not using Enum
  5. Prefer DI over getInstance() in any framework-managed application

Written for developers who don’t just want to know what Singleton is — but why it works, how it breaks, and when not to use it at all. If this helped you, check out the GenAI with Java series and the Interview Questions category on GangForCode.

See Also

Leave a Comment