Singleton pattern
The singleton restricts a class to one instance within a chosen scope and provides an access point. Thread-safe initialization, serialization safety, and testability are important design concerns.
Introduction
The Singleton pattern restricts a class to one instance within a defined scope and provides a way to access that instance. The scope matters: a manual singleton is usually one instance per class loader or process, while a dependency-injection container may define one instance per application context.
TL;DR / mental model: Make construction private, centralize instance creation, and make the lifecycle explicit. Use a singleton only when uniqueness is a real resource or coordination requirement; prefer dependency injection so callers receive the dependency instead of fetching global state.
Problem and Context
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.
When It Helps
Use it when multiple instances would represent the same process-wide resource or coordination point, such as a connection pool or hardware handle. If uniqueness is merely convenient, a regular object managed by dependency injection is usually easier to test and replace.
Participants and 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).
Idiomatic Example and Implementation Notes
The examples below show three common approaches. Each solves a different set of concerns, so choose based on initialization timing, inheritance, serialization, reflection, and testability rather than on a single universal ranking.
Which approach to learn first
Holder idiom is a concise lazy option. Enum singleton handles Java's enum serialization and reflection rules. Double-checked locking can be useful when explicit lazy initialization or constructor control is required, but it must use volatile and is easier to get wrong.
// 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 larger design concern is often testability. A singleton creates hidden global state, so unit tests can depend on each other unless that state is carefully reset. Thread safety is a separate implementation concern that can be addressed by several approaches.
// 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)
The key design insight is to use dependency injection so the singleton is wired at the composition root. Classes receive their dependencies; they do not fetch global state from inside business methods.
How It Works, Step by Step
The sequence diagram shows the holder idiom under concurrent access. JVM class initialization supplies the synchronization for this holder, so no additional application-level lock is needed for instance creation.
- 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 normally creates one instance per application context and injects it where needed. This is singleton scope managed by a DI container, not a manualgetInstance()implementation. - 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.
Trade-offs, Alternatives, and 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 but would otherwise make callers fetch global state (use DI to inject the instance)
- 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
The trade-off is centralized uniqueness in exchange for global-state coupling, lifecycle complexity, and harder substitution when callers invoke getInstance() directly. Alternatives include a regular object with one instance wired at the composition root, a static stateless utility for pure functions, or a provider/factory when instances may vary by scope. If the resource is truly singular, singleton scope can make sense; a DI container can usually manage that scope more safely than scattered manual access.
A practical default
Use dependency injection to manage singleton lifecycle when a container is available. If you implement it manually, choose an approach whose initialization, serialization, reflection, inheritance, and testability behavior you can explain. The holder idiom and enum approach are common Java choices with different constraints.
30-Second Explanation
A Singleton restricts construction so one instance exists within a chosen scope, then exposes access to that instance. The hard parts are safe initialization, visibility, serialization, reflection, lifecycle, and testability. In modern applications, dependency injection often provides singleton scope while keeping dependencies explicit.
5-Minute Explanation
Start with a private constructor and a static access path. The initialization-on-demand holder idiom uses JVM class initialization for lazy, thread-safe creation; double-checked locking can also work when the instance is volatile; an enum gets special serialization and reflection behavior but cannot extend another class. The pattern is appropriate for genuinely unique resources, not as a shortcut for passing dependencies. If callers use getInstance() throughout the codebase, hidden global state makes tests and replacement harder, so inject the singleton from the composition root instead.
Common Mistakes and Misconceptions
-
Writing the broken version and calling it done. The naive
if (instance == null)check is not thread-safe. Contrast it with a safe approach and make the initialization guarantee explicit. -
Using
synchronizedon the entire method without considering the trade-off. Synchronizing the whole method is simple and correct, but it serializes access to the method. Double-checked locking can avoid repeated lock acquisition after initialization, while the holder idiom is often simpler for a no-argument singleton. -
Forgetting
volatilein double-checked locking. Withoutvolatile, safe publication is not established and another thread may observe a non-null reference without the constructor's writes being safely visible. The field must bevolatilefor the standard pattern. -
Ignoring testability. A correct initialization strategy does not remove the hidden dependency created by
getInstance()calls. Prefer constructor injection so tests can provide a controlled instance. -
Treating enum singleton as interchangeable with every other approach. Enum singleton handles Java's enum serialization and reflective-construction rules with little boilerplate, but it cannot extend another class and is initialized with the enum class. The holder idiom is more flexible about inheritance and initialization timing.
Test Your Understanding
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 benefits from Java's special serialization and reflective-construction rules, but it cannot extend another class and is not as flexible about initialization.
- Testability is a major design concern: global state can make unit tests depend on one another and can hide dependencies.
- Use dependency injection to manage singleton scope when possible. The container defines the scope and injects the instance where needed.
- Choose among Holder, double-checked locking, enum, or container-managed scope based on initialization, inheritance, serialization, reflection, and testability requirements.
Related Patterns and Alternatives
| 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 and uniqueness is required | Math utils, string helpers | Application services when the framework-managed scope fits |
The key takeaway: static classes are for stateless utilities. Singletons are for stateful resources that must be unique. DI-managed singleton scope is often a useful application choice because the framework can handle lifecycle and injection.
Related Articles
The factory method replaces direct constructor calls with a method that subclasses can override to return different types. Callers depend on an abstraction, not a concrete class.
The strategy pattern extracts a family of algorithms behind an interface so the client can swap behaviors at runtime without touching the context class.