What are problems with Singleton
While the Singleton pattern ensures a class has only one instance, in modern enterprise software development it is widely classified as an architectural anti-pattern.
🟢 Junior Level
While the Singleton pattern ensures a class has only one instance, in modern enterprise software development it is widely classified as an architectural anti-pattern.
The Core Flaws of Singleton
- Destroys Unit Testability:
Private constructors and static
getInstance()access methods prevent mocking frameworks (such as Mockito) from substituting test doubles. Classes that rely on singletons cannot be tested in isolation:public class PaymentProcessor { public void charge(BigDecimal amount) { // ❌ Hardcoded dependency: Cannot mock without charging a real bank account! BankGateway.getInstance().charge(amount); } } - Hides Class Dependencies:
A class invoking
Singleton.getInstance()hides its external dependencies (databases, networks, file systems). Looking at its constructor signature reveals nothing about what resources the class needs to function. - Introduces Global Mutable State: If a Singleton holds state, it acts as a glorified global variable. Any component in the application can mutate that state, triggering unpredictable side effects across distant subsystems.
- Flaky Test Suites (State Leaks):
Because static fields live for the entire lifecycle of the JVM process, state mutations performed in
TestAleak intoTestB. Tests become order-dependent and fail intermittently when executed in parallel or shuffled order.
The Modern Alternative: Dependency Injection
Instead of handwritten Singleton classes, modern architectures rely on Inversion of Control (IoC) and Dependency Injection (DI) frameworks like Spring:
// ✅ Inversion of Control via Constructor Injection
@Service
public class PaymentProcessor {
private final BankGateway bankGateway;
public PaymentProcessor(BankGateway bankGateway) {
this.bankGateway = bankGateway; // Injected dependency: Trivially mocked in unit tests!
}
public void charge(BigDecimal amount) {
bankGateway.charge(amount);
}
}
In Spring, the bean is a standard POJO with a public constructor. Its singleton lifecycle is managed by the IoC container rather than hardcoded static references.
🟡 Middle Level
1. Tight Coupling and SOLID Violations
The classic Singleton violates virtually every principle of object-oriented design in SOLID:
| SOLID Principle | Violation Details |
|---|---|
| S (Single Responsibility) | The class mixes two orthogonal concerns: performing core business logic and rigidly enforcing its own lifecycle and instantiation policies. |
| O (Open/Closed) | Private constructors prevent inheritance and polymorphic extension. Behavior cannot be extended without modifying source code. |
| L (Liskov Substitution) | Callers hardcode direct references to MySingleton.getInstance(), making polymorphic subtype substitution impossible. |
| I (Interface Segregation) | Consumers couple directly to the monolithic concrete singleton class rather than granular, focused interfaces. |
| D (Dependency Inversion) | High-level business logic depends directly on low-level concrete static implementations instead of high-level abstractions. |
2. Thread Contention and Performance Bottlenecks
When a Singleton encapsulates mutable shared state (such as an in-memory cache, thread pool, or metrics collector), concurrent access requires synchronization:
public class UserSessionRegistry {
private static final UserSessionRegistry INSTANCE = new UserSessionRegistry();
private final Map<String, UserSession> sessions = new HashMap<>();
// ❌ Critical Bottleneck: Thousands of concurrent threads wait on ONE monitor lock!
public synchronized void registerSession(String token, UserSession session) {
sessions.put(token, session);
}
}
Under heavy load, thousands of concurrent HTTP requests queue up on a single class monitor, introducing severe lock contention and latency spikes.
3. The Distributed Fallacy in Cloud Architectures
The Singleton pattern guarantees uniqueness only within a single ClassLoader inside a single JVM process.
In cloud-native Kubernetes environments, applications run across multiple replicas:
Kubernetes Service Cluster
┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐
│ Pod #1 (JVM) │ │ Pod #2 (JVM) │ │ Pod #3 (JVM) │
│ SequenceGenerator = 1 │ │ SequenceGenerator = 1 │ │ SequenceGenerator = 1 │
└────────────────────────┘ └────────────────────────┘ └────────────────────────┘
If an engineer assumes SequenceGenerator.getInstance().nextId() produces globally unique identifiers across the system, running three pods generates duplicate primary keys and causes immediate data corruption.
[!IMPORTANT] In distributed systems, cluster-wide uniqueness must be delegated to external distributed coordination systems (e.g., PostgreSQL sequences, Redis
INCR, or ZooKeeper distributed locks), never JVM-local singletons.
4. ClassLoader Memory Leaks in Servlet Containers
In web containers (such as Apache Tomcat or Jetty), applications undergo “hot redeployment” without restarting the JVM:
- Tomcat creates a dedicated
WebappClassLoaderfor each deployed application. - If an application utilizes a static Singleton that registers background threads, listeners, or references to framework classes, the static reference acts as a GC Root.
- When the application is undeployed, the garbage collector attempts to reclaim the old
WebappClassLoader. However, the GC Root pins the classloader in memory. - Over multiple redeployments, memory builds up until the JVM crashes with:
java.lang.OutOfMemoryError: Metaspace (or PermGen in JDK < 8)
🔴 Senior Level
ClassLoader Deadlocks (JLS §12.4.2)
The Java Virtual Machine Specification dictates that class initialization is guarded by an internal Class Initialization Lock. If two singletons have circular static dependencies, concurrent initialization leads to an irrecoverable JVM ClassLoader deadlock:
public class ServiceA {
private static final ServiceA INSTANCE = new ServiceA();
static {
// Thread 1 initializes ServiceA and requests ServiceB
ServiceB.getInstance().doWork();
}
public static ServiceA getInstance() { return INSTANCE; }
}
public class ServiceB {
private static final ServiceB INSTANCE = new ServiceB();
static {
// Thread 2 initializes ServiceB and requests ServiceA
ServiceA.getInstance().doWork();
}
public static ServiceB getInstance() { return INSTANCE; }
}
Thread 1: Holds ClassInitLock(ServiceA) ──> Waiting on ClassInitLock(ServiceB) [BLOCKED]
Thread 2: Holds ClassInitLock(ServiceB) ──> Waiting on ClassInitLock(ServiceA) [BLOCKED]
===> JVM ClassLoader Deadlock at application boot!
In thread dumps, both threads appear in RUNNABLE or BLOCKED states inside classloader routines. No standard timeout mechanism can recover from this; the JVM freezes at startup.
Lack of Lifecycle Control vs. Spring Beans
Classic GoF Singletons lack lifecycle awareness:
- No Destruction Phase: When an application shuts down gracefully, a static Singleton receives no notification. Open database connection pools, un-flushed disk buffers, and network sockets leak.
- Spring Container Management: In contrast, Spring-managed singleton beans provide rich lifecycle hooks (
@PostConstruct,@PreDestroy,DisposableBean), deterministic dependency order resolution (@DependsOn), and graceful resource disposal.
Testing Legacy Code Bound to Singletons
When refactoring legacy systems where singletons cannot immediately be replaced with Dependency Injection, engineers employ two primary techniques:
- Mockito Static Mocking (Java 8+ / Mockito 3.4+):
try (MockedStatic<BankGateway> mockedGateway = mockStatic(BankGateway.class)) { BankGateway mockInstance = mock(BankGateway.class); mockedGateway.when(BankGateway::getInstance).thenReturn(mockInstance); paymentProcessor.charge(BigDecimal.valueOf(100)); verify(mockInstance).charge(BigDecimal.valueOf(100)); } - Subclass and Override (Package-Private Seam):
Wrap
BankGateway.getInstance()inside a protected method in the calling class and override it in test subclasses.
4 Tricky Questions
1. Why is the GoF Singleton considered a violation of the Dependency Inversion Principle (DIP)?
Answer: The Dependency Inversion Principle mandates that high-level business modules should not depend on low-level concrete classes; both should depend on abstractions (interfaces). Calling MyService.getInstance() directly in client code binds the client permanently to a specific concrete class and implementation. The client cannot substitute the service with a mocked variant, a caching proxy, or an alternative implementation without modifying the calling class’s source code.
2. What causes a Static Initialization Deadlock, and how does the JVM Class Initialization Lock behave?
Answer: According to JLS §12.4.2, every class or interface has a unique initialization lock that must be acquired by a thread executing its static initializers or static variable declarations. If Thread 1 triggers initialization of Class A (acquiring Lock A) and its static block invokes Class B, while Thread 2 simultaneously triggers initialization of Class B (acquiring Lock B) and its static block invokes Class A, a circular wait deadlock occurs. Because class loading locks are internal JVM monitors without exposure to thread timeout APIs, the JVM deadlocks permanently during application startup.
3. How does a Singleton cause OutOfMemoryError: Metaspace during application redeployment in Apache Tomcat?
Answer: In servlet containers, each web application is loaded by an isolated WebappClassLoader. Classes loaded by this classloader have their metadata stored in Metaspace. When a web application contains a static Singleton whose fields hold references to web application classes or thread context objects, that static field serves as an active GC Root. When the application is redeployed, the garbage collector cannot unload the old WebappClassLoader because the static GC Root keeps it reachable. Over multiple redeployments, zombie classloaders accumulate in Metaspace until an OutOfMemoryError: Metaspace crash occurs.
4. How does Spring implement the “Singleton” scope without inheriting the architectural flaws of the GoF Singleton?
Answer: Spring decouples the scoping lifecycle of an object from its class definition. A Spring singleton bean is an ordinary POJO with a public constructor and interface implementations. The Spring IoC container creates and retains a single instance in its internal bean registry (DefaultSingletonBeanRegistry). Because the class itself does not contain static methods or private constructors, developers can instantiate it freely, inject mocks in unit tests, configure lifecycle callbacks (@PostConstruct, @PreDestroy), and swap implementations via configuration.
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“The classic GoF Singleton is widely regarded as an anti-pattern because it introduces global mutable state, tightly couples callers to concrete implementations (violating SOLID’s SRP and DIP), and destroys unit testability by preventing mock injection. Furthermore, it fails to provide uniqueness across distributed microservice nodes (being strictly JVM-local) and can induce ClassLoader deadlocks and Metaspace memory leaks during redeployments. Modern enterprise development replaces manual singletons with Dependency Injection (Spring IoC), where singleton scope is managed by the container without static global state.”
Key Architectural Trade-offs
- GoF Singleton vs Spring Singleton: GoF enforces 1 instance per ClassLoader via static methods and private constructors (rigid, untestable). Spring manages 1 instance per IoC container via POJOs and dependency injection (flexible, mockable, lifecycle-aware).
- Global State vs Explicit Dependencies: Favor explicit constructor injection over hidden static lookups.
- JVM Uniqueness vs Distributed Uniqueness: Never rely on singletons for cluster-wide consistency; use external databases or distributed stores like Redis and ZooKeeper.
Red Flags & Anti-Patterns
- 🚩 Writing manual Singletons in Spring Boot applications: Creating static singletons instead of standard
@Serviceor@Componentbeans. - 🚩 Using Singletons for distributed cluster tasks: E.g., distributed ID generation or cluster-wide rate limiting via JVM static counters.
- 🚩 Allowing mutable shared state without synchronization: Creates subtle multithreaded data races.
- 🚩 Excusing untested code because “it uses a Singleton”: Use constructor injection refactoring or
mockStatic()in Mockito.
Related Topics
- What is Singleton — Core mechanics, reflection attacks, and serialization caveats
- How to implement thread-safe Singleton — Enum and Bill Pugh implementations
- What is double-checked locking — Volatile semantics and memory barriers
- What anti-patterns do you know — God Object, Tight Coupling, and Premature Optimization