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.

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
- Private constructor — prevents external instantiation via
new - Private static instance variable — holds the single instance
- 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)
| Class | How accessed |
|---|---|
java.lang.Runtime | Runtime.getRuntime() |
java.awt.Desktop | Desktop.getDesktop() |
java.lang.System | Static methods only |
java.util.logging.LogManager | LogManager.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():
- Allocate memory
- Assign reference to
instance← reordering can put this before step 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).
6. Bill Pugh — Inner Static Holder (Recommended)
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.
| Implementation | Lazy? | 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, throwingIllegalArgumentException. 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:
- Allocate memory for the object
- Initialise the object’s fields (run the constructor)
- Assign the memory address to the
instancereference
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
- Can two threads enter the constructor simultaneously? (mutual exclusion)
- 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 Singleton | Spring Singleton | |
|---|---|---|
| Scope | One per JVM | One per ApplicationContext |
| Enforcement | Private constructor | Container-managed |
| Thread safety | Developer’s responsibility | Developer’s responsibility |
| Testability | Hard (global state) | Easy (inject mocks) |
| Multiple instances? | Impossible by design | Possible 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?”
| Singleton | Static Class | |
|---|---|---|
| Instantiation | One instance | Never instantiated |
| Inheritance | Can implement interfaces, extend classes | Cannot |
| State | Can hold mutable instance state | Only static fields |
| Lazy init | Possible | Not applicable |
| Testability | Can be mocked | Hard to mock/replace |
| Polymorphism | Supported | Not supported |
| Serialization | Supported | Not 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:
- Refactor to DI: Pass the dependency as a constructor argument instead of calling
getInstance(). Then inject a mock in tests. - Reflection: Reset the static
instancefield to null between tests usingField.setAccessible(true). - 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
| Scenario | Recommended Implementation |
|---|---|
| Simple, stateless singleton | Eager initialisation |
| Lazy, high-performance, no reflection concerns | Bill Pugh (inner static holder) |
| Lazy + high-throughput multithreaded | DCL with volatile |
| Maximum safety (reflection + serialization proof) | Enum Singleton |
| Spring application | Just use @Service / @Component — let Spring manage it |
| Needs to implement an interface | Bill Pugh or DCL |
The Five Rules to Live By
- Use
volatilein DCL — always, no exceptions - Prefer Bill Pugh over
synchronizedmethods — no lock on the hot path - Use Enum when you need reflection and serialisation safety with zero effort
- Implement
readResolve()if your Singleton is Serializable and you’re not using Enum - 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.