Singleton pattern
The singleton ensures a class has exactly one instance with a global access point. Thread-safe initialization, serialization safety, and testability are the three problems most implementations get wrong.
The Problem It Solves
You need exactly one database connection pool for your entire application. Multiple pools would waste memory, exhaust database connections, and create inconsistent state. The naive solution looks simple but breaks under concurrency.
If two threads call getInstance() at the same moment, both see null, both create a pool, and one silently overwrites the other. You now have two pools, leaked connections, and a mystery production bug. This is the race condition every singleton discussion starts with.
The pattern seems trivial until you consider thread safety, serialization, reflection, and testability. Those four concerns separate a working singleton from a broken one.
Here is what changes when you apply the Singleton pattern.
Structure
The private constructor prevents anyone from calling new. The Holder inner class leverages the JVM's class-loading guarantee: a class is initialized exactly once, and initialization is thread-safe without explicit synchronization. The pool is created only when getInstance() is first called (lazy) and only once (thread-safe).
Implementation
I will show three correct approaches in order of my preference. Each solves different concerns.
Which approach to learn first
Holder idiom for most production code. Enum singleton when serialization or reflection matter. Double-checked locking when you need to understand the memory model (interviewers love asking about volatile).
// APPROACH 1: Initialization-on-demand holder idiom.
// Lazy, thread-safe, no synchronized keyword, no volatile.
// The JVM spec (Β§12.4.2) guarantees class initialization
// is atomic and happens-before any thread reads static fields.
public class DatabaseConnectionPool {
private final String jdbcUrl;
private final int poolSize;
// Private constructor: nobody outside can instantiate
private DatabaseConnectionPool() {
this.jdbcUrl = System.getenv("DB_URL");
this.poolSize = Integer.parseInt(
System.getenv().getOrDefault("POOL_SIZE", "10"));
System.out.println("Pool created with " + poolSize + " connections");
}
// Inner class is NOT loaded until getInstance() is called.
// When it IS loaded, JVM guarantees single-threaded init.
private static final class Holder {
static final DatabaseConnectionPool INSTANCE =
new DatabaseConnectionPool();
}
public static DatabaseConnectionPool getInstance() {
return Holder.INSTANCE;
}
public String getJdbcUrl() { return jdbcUrl; }
public int getPoolSize() { return poolSize; }
}The Testability Problem
The biggest criticism of singleton is not thread safety (that is solved). It is testability. Singleton creates hidden global state that makes unit tests depend on each other.
// Problem: tests that use ConfigService.getInstance() share state.
// Test A modifies the singleton, Test B runs next and unexpectedly
// sees Test A's changes. Test order matters. Parallel tests break.
// Solution: Dependency Injection. Pass the instance, don't fetch it.
public class OrderService {
private final ConfigService config; // injected, not fetched
// Constructor injection: tests control which instance is used
public OrderService(ConfigService config) {
this.config = config;
}
public int getMaxRetries() {
return Integer.parseInt(config.get("max.retries"));
}
}
// In production: new OrderService(ConfigService.getInstance())
// In tests: new OrderService(mockConfigService)
In an interview, this is the key insight: "I know how to implement singleton correctly, but in production I would use dependency injection so the singleton is wired at the composition root. Classes receive their dependencies, they don't fetch globals."
How It Works
The sequence diagram shows the holder idiom under concurrent access. The JVM handles thread safety, so no application-level synchronization is needed.
- Thread A calls
getInstance(), which referencesHolder.INSTANCEfor the first time. - The JVM loads the
Holderclass. The JVM specification guarantees this happens atomically. No application synchronization needed. - During class loading,
INSTANCEis initialized. TheDatabaseConnectionPoolconstructor runs exactly once. - Thread B calls
getInstance()concurrently. If class loading is in progress, Thread B blocks on the JVM's internal lock. If done, Thread B reads the fully-constructed instance. - Both threads get the same instance. The happens-before relationship of class initialization guarantees all fields are visible.
Real-World Examples
java.lang.Runtime.getRuntime()returns the singletonRuntimeinstance. There is exactly one runtime per JVM process, managing memory and external processes.- Spring's default bean scope is singleton. When you annotate a class with
@Service, Spring creates one instance and injects it everywhere. This is singleton via DI container, not viagetInstance(). - SLF4J's
LoggerFactorymaintains a singleton logging framework binding. One binding per application, selected at startup, used by every class that callsLoggerFactory.getLogger(). java.awt.Desktop.getDesktop()returns a singleton representing the user's desktop environment. Only one desktop exists per user session.
When to Use / When NOT to Use
Use when:
- The resource is inherently singular (connection pool, thread pool, hardware interface)
- Creating multiple instances would cause resource exhaustion or state conflicts
- You need a global coordination point (registry, cache, configuration)
Skip when:
- You want testability (use DI to inject the instance instead of calling
getInstance()) - The class has mutable state that tests modify (tests will interfere with each other)
- You are using it as a glorified global variable to avoid passing parameters
If the resource is truly singular (one pool, one config), singleton makes sense as a lifecycle mechanism. But let the DI container manage it, not getInstance() calls scattered through your codebase.
The interview answer
"I would use dependency injection to manage the singleton lifecycle. If forced to implement it manually, enum singleton is the safest Java approach. But I understand the holder idiom and double-checked locking for when the interviewer wants to discuss the memory model."
Common Mistakes in Interviews
-
Writing the broken version and calling it done. The naive
if (instance == null)check is not thread-safe. If an interviewer asks for singleton, they expect you to explain the race condition and fix it. Start with the broken version on purpose, then upgrade. -
Using
synchronizedon the entire method. This makes every call pay the synchronization cost, even after the instance exists. Double-checked locking avoids this. Say: "Synchronizing the whole method works but is slower. Double-checked locking synchronizes only the first creation." -
Forgetting
volatilein double-checked locking. Withoutvolatile, the JVM can reorder the write toinstancebefore the constructor finishes. Thread B sees a non-null reference but a partially-constructed object. This is the most commonly missed detail. -
Not mentioning testability. Candidates who only describe implementation miss the bigger picture. Always say: "The real problem with singleton is testability. In production, I inject the instance via constructor instead of calling
getInstance()directly." -
Dismissing enum singleton as weird. Enum singleton is Josh Bloch's explicit recommendation in Effective Java. It handles serialization and reflection with zero boilerplate. Say: "Enum singleton covers edge cases that the holder idiom does not, specifically serialization and reflection attacks."
Singleton vs Static Class vs Dependency Injection
| Aspect | Singleton | Static Class | DI-Managed Singleton |
|---|---|---|---|
| Instance | One, accessed via getInstance() | No instance, all static | One, injected by framework |
| State | Can hold mutable state | Stateless (or dangerous static fields) | Can hold mutable state |
| Testability | Hard (global state, hidden deps) | Easy (pure functions) | Easy (inject mock) |
| Polymorphism | Can implement interfaces | Cannot | Can implement interfaces |
| Lazy init | Holder idiom or DCL | Loaded when class loads | Framework controls timing |
| Use case | No DI framework available | Math utils, string helpers | Production services (preferred) |
The key takeaway: static classes are for stateless utilities. Singletons are for stateful resources that must be unique. DI-managed singletons are the production-preferred approach where the framework handles lifecycle and testability.
Test Your Understanding
Quick Recap
- The naive
if (instance == null)singleton has a race condition. Two threads can create two instances if they interleave at the null-check. - The Holder idiom (inner static class) is lazy, thread-safe, and needs no
synchronizedorvolatile. It relies on the JVM's class-loading guarantee. - Double-checked locking needs
volatileto prevent partial construction visibility. Without it, another thread can see a non-null but incomplete object. - Enum singleton is serialization-safe and reflection-safe. It is Josh Bloch's recommendation in Effective Java.
- The real problem with singleton is testability, not thread safety. Global state makes unit tests depend on each other and prevents mocking.
- In production, use dependency injection to manage singleton lifecycle. The DI container creates one instance and injects it everywhere.
- In an interview, show the broken version first, explain the race condition, then present the fix. Mention testability and DI to differentiate yourself.