The Singleton Pattern Deep Dive

Site Admin · 11 Sep 2026 · 12 views

The Singleton Pattern

The Singleton pattern restricts a class to a single instance and provides a global point of access to it. It is one of the most well-known patterns, but also one of the most abused. Knowing when to use it - and when NOT to use it - is critical.

Why a Single Instance?

Some objects are expensive to create or must coordinate across the entire application. Database connection pools, configuration managers, and logging services are natural candidates. Multiple instances would waste resources or cause inconsistent state.

+------------------------------+
|      AppConfig (Singleton)   |
|  private static INSTANCE     |
|  private AppConfig()         |
|  public static getInstance() |
+------------------------------+
              ^
              |
      +-------+------+
      |              |
    getInstance    getInstance
      |              |
   same instance   same instance

Two callers anywhere in the program call getInstance() and always receive the exact same object.

Basic Eager Initialization

The simplest approach creates the instance when the class loads.

public class AppConfig {
    private static final AppConfig INSTANCE = new AppConfig();
    private String appName;

    private AppConfig() {
        this.appName = "GroovyGrails Hub";
    }

    public static AppConfig getInstance() {
        return INSTANCE;
    }

    public String getAppName() {
        return appName;
    }
}

Walkthrough: the private constructor blocks external instantiation. The static final field INSTANCE is created once at class-load time, so it is inherently thread-safe - the JVM guarantees only one instance exists. getInstance() simply returns it. The only downside: the object exists even if it is never used.

Double-Checked Locking

If creation is expensive, you can delay it until first use. Double-checked locking synchronizes only the first creation.

public class DatabasePool {
    private static volatile DatabasePool instance;
    private final List<Connection> pool;

    private DatabasePool() {
        pool = new ArrayList<>();
        // initialize connections
    }

    public static DatabasePool getInstance() {
        if (instance == null) {                    // first check
            synchronized (DatabasePool.class) {
                if (instance == null) {            // second check
                    instance = new DatabasePool();
                }
            }
        }
        return instance;
    }
}

The volatile keyword makes the reference visible across threads. The first null check avoids locking once the instance exists. The second check inside the synchronized block stops two threads from creating two instances in a race.

Enum Singleton (Recommended)

Joshua Bloch recommends an enum-based Singleton. The JVM guarantees one instance, and serialization and reflection attacks are handled automatically.

public enum Logger {
    INSTANCE;

    private final List<String> logs = new ArrayList<>();

    public void log(String message) {
        logs.add(message);
        System.out.println("[LOG] " + message);
    }

    public List<String> getLogs() {
        return new ArrayList<>(logs);
    }
}

Usage is dead simple: Logger.INSTANCE.log("started"). Enums cannot be instantiated again by reflection, and serialization preserves the single instance.

Use Cases and Pitfalls

Real-world use: logging frameworks, configuration objects, thread pools, and caches. The pitfall is that a Singleton is effectively a global - classes calling Logger.INSTANCE directly are tightly coupled and hard to unit test because you cannot inject a mock. In application code, prefer passing dependencies through constructors, and reserve Singleton for genuinely resource-level objects such as connection pools.

Key Points

  • Singleton guarantees one instance and a global access point.
  • Eager initialization is simplest and thread-safe via class-loading guarantees.
  • Double-checked locking with volatile enables safe lazy creation.
  • The enum approach is the most robust and is the recommended practice.
  • Use it for shared resources; avoid it when it creates hidden coupling and hurts testability.
Share this post:

Comments (0)

Please login or register to comment.