What Are Problems with Singleton?
Singleton problems fall into three categories: testability (cannot mock), architecture (hidden dependencies), infrastructure (scaling issues).
Junior Level
Singleton problems fall into three categories: testability (cannot mock), architecture (hidden dependencies), infrastructure (scaling issues).
- Hard to test — cannot substitute with a mock
- Hidden dependencies — not visible that a class uses Singleton
- Global state — tests affect each other
- Doesn’t scale — in a cluster, each node gets its own Singleton
Example of the problem:
// Singleton interferes with tests
public class OrderService {
public void create() {
Database.getInstance().save(...); // Real DB!
}
}
@Test
void testOrder() {
// Impossible to substitute Database with a test one!
// Test goes to real DB
}
Solution: Instead of manual Singleton, use a DI container (Spring) that manages instance uniqueness for you.
Spring @Component + singleton scope = the same Singleton, but testable.
Middle Level
1. Testing Problems
Flaky Tests — tests that randomly pass/fail without code changes.
// Singleton = Flaky Tests
@Test
void test1() {
Counter.getInstance().increment();
assertEquals(1, Counter.getInstance().getCount());
}
@Test
void test2() {
// Counter already = 1 from test1!
assertEquals(1, Counter.getInstance().getCount()); // FAIL!
}
// Solution: DI
public class Counter {
private int count = 0;
public void increment() { count++; }
public int getCount() { return count; }
}
@Test
void test1() {
Counter counter = new Counter(); // New for each test
counter.increment();
assertEquals(1, counter.getCount());
}
2. Tight Coupling
// Dependency is hidden
public class OrderService {
public void process() {
Logger.getInstance().log("..."); // Hidden dependency
Config.getInstance().getTimeout(); // Another one
Cache.getInstance().get("key"); // And another
}
}
// Looking at the constructor — it's unclear what's needed for it to work!
// Explicit dependencies through constructor
public class OrderService {
private final Logger logger;
private final Config config;
private final Cache cache;
public OrderService(Logger logger, Config config, Cache cache) {
this.logger = logger;
this.config = config;
this.cache = cache;
}
}
// Immediately clear what's needed
3. Lock Contention
// Singleton with state = bottleneck
public class SessionManager {
private final Map<String, Session> sessions = new HashMap<>();
public synchronized void addSession(String id, Session session) {
sessions.put(id, session); // All threads wait!
}
}
// At 1000 req/sec -> queue to synchronized method
4. Memory Leaks
Metaspace — JVM memory area for class metadata.
// Singleton lives forever
public class DataLoader {
private static DataLoader instance;
private List<Data> cache = new ArrayList<>(); // Grows!
private DataLoader() {}
public static DataLoader getInstance() {
if (instance == null) instance = new DataLoader();
return instance;
}
}
// In Tomcat on redeploy:
// Old ClassLoader not GC -> Metaspace Leak
// -> OutOfMemoryError: Metaspace
5. Doesn’t Work in a Cluster
Node 1: Singleton -> counter = 1
Node 2: Singleton -> counter = 1
Node 3: Singleton -> counter = 1
// Expected: one counter across all nodes
// Reality: one counter per node!
Solution: Dependency Injection
// Spring manages the lifecycle
@Component
public class UserService {
private final UserRepository repo;
public UserService(UserRepository repo) {
this.repo = repo; // Explicit dependency
}
}
// Easy to substitute in tests
@Test
void test() {
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo);
// Testing in isolation
}
Senior Level
Architectural Degradation
SOLID Violations:
| Principle | How Singleton Violates It |
|---|---|
| Single Responsibility | Class manages both logic and its lifecycle |
| Open/Closed | Cannot extend without modification |
| Liskov Substitution | Cannot substitute with a subclass |
| Interface Segregation | Dependent on concrete implementation |
| Dependency Inversion | Depends on concrete class, not abstraction |
Static Initialization Deadlocks
// Mutual dependency in static initializers
public class A {
static { B.getInstance(); } // Waits for B
private static A instance = new A();
public static A getInstance() { return instance; }
}
public class B {
static { A.getInstance(); } // Waits for A
private static B instance = new B();
public static B getInstance() { return instance; }
}
// Thread 1: loads A -> waits for B
// Thread 2: loads B -> waits for A
// -> DEADLOCK at Class Loading stage!
// -> Very hard to catch
Horizontal Scaling
// Singleton is useless for distributed IDs
public class IdGenerator {
private static long counter = 0;
public static synchronized long nextId() { return ++counter; }
}
// In a 5-node cluster:
// Node 1: 1, 2, 3
// Node 2: 1, 2, 3 <- Duplicates!
// Node 3: 1, 2, 3
// Solution: distributed generator
// Redis INCR, Snowflake ID, UUID
Lifecycle Management
// Singleton has no shutdown()
public class ConnectionPool {
private static ConnectionPool instance;
private List<Connection> connections;
private ConnectionPool() {
connections = createConnections(); // Opened 10 connections
}
// How to close on application shutdown?
// No standard method!
}
// Spring: @PreDestroy
@Component
public class ConnectionPool {
@PreDestroy
public void shutdown() {
connections.forEach(Connection::close);
}
}
Thread Contention in Highload
// Mutable Singleton = bottleneck
public class MetricsCollector {
private static MetricsCollector instance;
private final Map<String, Long> metrics = new ConcurrentHashMap<>();
public void increment(String name) {
metrics.merge(name, 1L, Long::sum); // ConcurrentHashMap, but...
}
public Map<String, Long> getMetrics() {
return new HashMap<>(metrics); // ...copy every time!
}
}
// At 10,000 req/sec:
// ConcurrentHashMap reduces contention, but copy in getMetrics() creates 10,000 objects/sec -> GC pressure.
// Solution: ThreadLocal or distributed metrics
ClassLoader Memory Leaks Deep Dive
Tomcat redeploy:
1. Old webapp ClassLoader should be GC'd
2. But Singleton.class has a static field -> reference
3. ClassLoader cannot be GC'd
4. All webapp classes stay in memory
5. Metaspace grows -> OutOfMemoryError
// Solution:
// 1. DON'T use static Singletons in webapp
// 2. Use DI container (Spring)
// 3. Spring cleans up on shutdown itself
Production Experience
Real scenario #1: Singleton killed scaling
- Rate Limiter as Singleton
- 5 nodes in cluster
- Expected: 100 req/min across all nodes
- Reality: 100 req/min PER NODE (500 total!)
- Solution: Redis-based rate limiter
Real scenario #2: Static deadlock in production
- Two Singletons initializing each other
- Deadlock at startup -> application hung
- 4 hours of debugging -> found via thread dump
- Solution: removed circular dependency
Real scenario #3: Memory Leak
- Metaspace grew by 50 MB on each deploy
- After 10 deploys -> OutOfMemoryError
- Cause: Singleton held ClassLoader
- Solution: migrated to Spring DI
Best Practices
- DON’T use manual Singletons in Spring applications
- DI Container manages lifecycle better
- Avoid mutable state in Singleton
- For clusters -> distributed solutions (Redis, ZK)
- Monitor Metaspace when using static
ZK (Zookeeper) — distributed coordination system.
- @PreDestroy for proper shutdown
- ThreadLocal to reduce contention
- Explicit dependencies through constructor
Senior Summary
- SOLID violations: Singleton violates 5 out of 6 principles
- Static deadlocks: hard to catch, catastrophic
- ClassLoader leaks: Metaspace OOM on redeploy
- Cluster myth: Singleton != distributed uniqueness
- Thread contention: mutable state = bottleneck
- Lifecycle: no standard shutdown mechanism
- DI Container solves ALL these problems
- Rule: Singleton only for stateless utilities or SDKs
Interview Cheat Sheet
Must know:
- Singleton violates SOLID: SRP (lifecycle management), DIP (hidden dependencies), OCP (cannot extend), LSP (cannot substitute), ISP (concrete dependency)
- Untestable: cannot mock, state between tests -> flaky tests
- Static initialization deadlocks — two Singletons initialize each other -> deadlock at startup
- ClassLoader memory leaks: in Tomcat on redeploy, Singleton prevents GC -> Metaspace OOM
- In a cluster, Singleton is useless — each node creates its own instance
- Mutable state in Singleton = thread contention bottleneck
- DI Container solves ALL these problems
Common follow-up questions:
- Which SOLID principles does Singleton violate? — SRP (dual responsibility), DIP (hidden dependencies), OCP (cannot extend), LSP (cannot substitute)
- What is a static initialization deadlock? — Two classes in static blocks wait for each other -> deadlock at Class Loading stage
- Why does Singleton leak in Tomcat? — Static field holds ClassLoader -> ClassLoader not GC’d -> Metaspace grows
- How to solve testability? — DI: passing dependencies through constructor instead of Singleton.getInstance()
Red flags (DO NOT say):
- “Singleton is perfectly testable” — cannot mock, state between tests
- “I don’t use DI, Singleton is simpler” — hidden dependencies = technical debt
- “In a cluster, Singleton solves coordination” — each node creates its own instance
- “Static deadlocks are impossible” — possible with circular dependency of static initializers
Related topics:
- [[03. What is Singleton]] — general pattern description
- [[04. How to implement thread-safe Singleton]] — safe implementations
- [[05. What is double-checked locking]] — DCL optimization
- [[02. What pattern categories exist]] — Creational patterns
- [[16. What anti-patterns do you know]] — Singleton as an anti-pattern