Design Patterns in Java

Singleton Pattern

15 min Lesson 2 of 13

Singleton Pattern

The Singleton pattern guarantees that a class has exactly one instance throughout the lifetime of the application, and provides a single global access point to that instance. It is one of the most widely used — and most frequently misused — creational patterns in the GoF catalogue.

When is a Singleton appropriate?

A class should be a singleton when two conditions are both true:

  • Exactly one instance must exist — e.g. a configuration registry, a connection pool, a thread-safe cache, a hardware interface driver.
  • That single instance must be accessible from many places without passing it through every constructor or method call.
Singleton vs. static utility class: A static class cannot implement an interface, cannot be passed as a dependency, and cannot be replaced in tests. A Singleton is still an object — it can implement interfaces, be injected, and be mocked. Prefer Singleton over a static class whenever polymorphism or testability matters.

Eager Initialization

The simplest form: the instance is created when the class is loaded by the JVM. Because class loading is thread-safe by the JVM spec, no synchronization is needed.

public final class AppConfig { // created once, at class-load time private static final AppConfig INSTANCE = new AppConfig(); private final String dbUrl; private AppConfig() { // load from environment or properties file this.dbUrl = System.getenv().getOrDefault("DB_URL", "jdbc:h2:mem:test"); } public static AppConfig getInstance() { return INSTANCE; } public String getDbUrl() { return dbUrl; } }

Trade-off: The instance is created even if it is never used. For lightweight objects this is fine. For expensive resources (e.g. a database connection pool) that might not always be needed, lazy initialization is preferable.

Lazy Initialization — the Double-Checked Locking idiom

Lazy initialization defers creation until the first call to getInstance(). In a concurrent environment a naive if (instance == null) check is not safe — two threads can both see null and each create an instance. The classic fix is double-checked locking combined with the volatile keyword:

public final class ConnectionPool { // volatile ensures the write to INSTANCE is visible to all threads // before the reference escapes private static volatile ConnectionPool INSTANCE; private final int maxConnections; private ConnectionPool() { this.maxConnections = Integer.parseInt( System.getenv().getOrDefault("POOL_SIZE", "10") ); } public static ConnectionPool getInstance() { if (INSTANCE == null) { // 1st check — no lock synchronized (ConnectionPool.class) { if (INSTANCE == null) { // 2nd check — under lock INSTANCE = new ConnectionPool(); } } } return INSTANCE; } public int getMaxConnections() { return maxConnections; } }
Why is volatile mandatory here? Without it the JVM is allowed to reorder the write to INSTANCE so that another thread might observe a partially-constructed object. volatile imposes a happens-before relationship: the full construction completes before the reference is published. Omitting volatile is a subtle data race that causes hard-to-reproduce bugs on multi-core hardware.

The Initialization-on-Demand Holder idiom

A cleaner lazy alternative that avoids explicit synchronization entirely, exploiting the JVM guarantee that a class is initialized at most once and only when first accessed:

public final class MetricsRegistry { private MetricsRegistry() {} // The JVM initializes this inner class only when Holder.INSTANCE is first read. // Class initialization is inherently thread-safe — no volatile, no lock needed. private static final class Holder { static final MetricsRegistry INSTANCE = new MetricsRegistry(); } public static MetricsRegistry getInstance() { return Holder.INSTANCE; } }
Prefer the Holder idiom over double-checked locking for new code. It is shorter, easier to reason about, and equally lazy and thread-safe. Double-checked locking is still found extensively in legacy codebases so you must be able to read and fix it.

The Enum Singleton — the gold standard

Joshua Bloch's Effective Java (Item 3) recommends implementing Singleton as a single-element enum. This is the most concise, thread-safe, and serialization-safe approach available in Java:

public enum AuditLogger { INSTANCE; // state is fine — enums are objects private final List<String> log = new java.util.ArrayList<>(); public void record(String event) { synchronized (log) { log.add(java.time.Instant.now() + " " + event); } } public List<String> getLog() { synchronized (log) { return List.copyOf(log); } } } // usage — no getInstance() needed AuditLogger.INSTANCE.record("Payment processed");

The enum approach defeats three classic singleton-breaking attacks:

  1. Reflection: Constructor.setAccessible(true) throws an IllegalArgumentException for enum constructors — the JVM itself enforces this.
  2. Serialization: By default, deserializing a class produces a new instance, breaking the invariant. Enum serialization is handled by the JVM and always returns the single canonical instance — no readResolve() override needed.
  3. Cloning: Enum does not implement Cloneable, so clone() throws CloneNotSupportedException.
Why not always use enum? Enums cannot extend a superclass (they implicitly extend java.lang.Enum). If your Singleton must inherit from an abstract base class or be created lazily with complex initialization logic, use the Holder idiom instead. But for the common case — a stateless or simply-stateful singleton — the enum form is ideal.

Thread Safety Comparison

  • Eager init: Thread-safe; no extra work required. Instance always created.
  • Double-checked locking: Thread-safe with volatile; lazy. Common in legacy code.
  • Holder idiom: Thread-safe via JVM class-init semantics; lazy. Recommended for class-based singletons.
  • Enum singleton: Thread-safe; serialization-safe; reflection-proof. Recommended by Effective Java.

Singletons and Dependency Injection

A common professional pattern is to combine the Singleton scope with DI. Frameworks like Spring manage singleton beans for you — you declare a @Service or @Component and the container ensures one instance. You gain all the testability of DI without hand-rolling double-checked locking.

When you write raw Singletons (outside a DI container), program to an interface so callers depend on the abstraction, not the concrete singleton class. This preserves testability:

public interface ConfigProvider { String get(String key); } public enum AppSettings implements ConfigProvider { INSTANCE; private final java.util.Properties props = new java.util.Properties(); AppSettings() { try (var in = getClass().getResourceAsStream("/app.properties")) { if (in != null) props.load(in); } catch (java.io.IOException e) { throw new ExceptionInInitializerError(e); } } @Override public String get(String key) { return props.getProperty(key); } }

Summary

Use eager initialization when the cost of creating the instance is low. Use the Holder idiom when lazy initialization for a class-based singleton is required. Use the enum singleton as the default choice for any new singleton that does not need to extend a class — it is concise, thread-safe, serialization-safe, and reflection-proof. Understand double-checked locking to maintain legacy code safely.