Question 6 · Section 2

What Are Problems with Singleton?

Singleton problems fall into three categories: testability (cannot mock), architecture (hidden dependencies), infrastructure (scaling issues).

Language versions: English Russian Ukrainian

Junior Level

Singleton problems fall into three categories: testability (cannot mock), architecture (hidden dependencies), infrastructure (scaling issues).

  1. Hard to test — cannot substitute with a mock
  2. Hidden dependencies — not visible that a class uses Singleton
  3. Global state — tests affect each other
  4. 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

  1. DON’T use manual Singletons in Spring applications
  2. DI Container manages lifecycle better
  3. Avoid mutable state in Singleton
  4. For clusters -> distributed solutions (Redis, ZK)
  5. Monitor Metaspace when using static

ZK (Zookeeper) — distributed coordination system.

  1. @PreDestroy for proper shutdown
  2. ThreadLocal to reduce contention
  3. 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