Chapter 03 · Article 09 of 55

Singleton Design Pattern

Intent: Ensure a class has only one instance and provide a global point of access to it.

Article outline15 sections on this page

Overview

Intent: Ensure a class has only one instance and provide a global point of access to it.

The Singleton pattern is a creational design pattern that restricts the instantiation of a class to exactly one object. It provides a well-known access point to that single instance so that all clients share the same object throughout the application lifecycle.

Core Guarantees:

  • Single Instance - Only one object of the class exists in the JVM (or runtime environment).
  • Global Access Point - A static method (getInstance()) provides access from anywhere in the codebase.
  • Controlled Instantiation - The constructor is private; no external code can create a new instance.

The pattern is deceptively simple in concept but notoriously tricky to implement correctly in multithreaded environments - which is precisely why it dominates interview discussions.

Historical Context: Introduced by the Gang of Four (GoF) in 1994, Singleton remains the most frequently asked design pattern in software engineering interviews. Its simplicity invites follow-up questions about concurrency, serialization, and architectural trade-offs that reveal a candidate's depth of understanding.

How it works at a high level:

  1. Declare the constructor private so no external class can call new.
  2. Create a private static field to hold the single instance.
  3. Expose a public static method (getInstance()) that creates the instance on first call (lazy) or returns the pre-created instance (eager).

Problem It Solves

Certain resources in a system must be shared across components without creating multiple conflicting instances:

Problem DomainWhy Singleton Helps
Configuration ManagerOne source of truth for app settings; avoids inconsistent reads from multiple config objects
Database Connection PoolPools are expensive to create; multiple pools waste connections and memory
Logging ServiceAll modules should write to the same log destination with consistent formatting
Cache ManagerA single shared cache avoids duplication and stale data across layers
Hardware Interface AccessPrinter spooler, file system handle - physical resources cannot be multiplexed arbitrarily
Registry / Service LocatorCentral lookup for services must be unique to avoid conflicting registrations

Without Singleton, developers risk creating multiple instances that compete for shared resources, produce inconsistent state, or waste memory.


When to Use / When NOT to Use

When to UseWhen NOT to Use
Exactly one instance is needed system-wideObject carries mutable state that varies per context
Shared access to a costly resource (DB pool, thread pool)You need multiple instances for testing or multi-tenancy
Coordinating actions across the system (event bus, scheduler)The class has no shared state - a static utility class suffices
Replacing global variables with controlled accessYou're using it just to avoid passing dependencies (use DI instead)
Lazy initialization of expensive objects is desiredThe object's lifecycle should be managed by a framework (Spring, Guice)
Read-heavy configuration that rarely changesYou need polymorphism or interface-based substitution in tests

Rule of thumb: If you find yourself needing to mock or replace the Singleton in unit tests, it's a signal that Dependency Injection is a better fit.


Key Concepts & Theory

Lazy vs Eager Initialization

  • Eager: Instance is created at class-loading time. Simple, thread-safe by default (JVM guarantees), but wastes memory if never used.
    private static final Singleton INSTANCE = new Singleton(); // eager
    
  • Lazy: Instance is created on first call to getInstance(). Saves resources but introduces thread-safety complexity. Preferred when the object is expensive to create and may not be needed in every execution path.

Trade-off Matrix:

EagerLazy
Thread SafetyFree (JVM)Must implement
MemoryAllocated at startupOn-demand
Startup TimeSlowerFaster
ComplexityTrivialModerate to High

Thread Safety

In a multithreaded environment, two threads can simultaneously enter getInstance() before the instance is assigned, creating two objects. Solutions include synchronization, double-checked locking, and leveraging the JVM classloader.

The race condition scenario:

Thread A: checks instance == null → true
Thread B: checks instance == null → true (A hasn't assigned yet)
Thread A: creates instance #1
Thread B: creates instance #2  ← VIOLATION

The volatile keyword in DCL prevents instruction reordering. Without it, the JVM may assign the reference before the constructor completes, exposing a half-initialized object to another thread.

Serialization Concerns

When a Singleton is serialized and then deserialized, the default mechanism creates a new object, breaking the single-instance guarantee. The fix is implementing readResolve() to return the existing instance.

Reflection Attacks

Java Reflection can invoke private constructors, bypassing the Singleton contract. Defensive code in the constructor (throwing an exception if an instance already exists) or using the Enum approach prevents this.

Cloning

If the Singleton class extends Cloneable, calling clone() produces a second instance. Override clone() to throw CloneNotSupportedException.


ASCII Class Diagram

┌─────────────────────────────────────┐
│           <<Singleton>>             │
│─────────────────────────────────────│
│ - instance : Singleton [static]     │
│─────────────────────────────────────│
│ - Singleton()                       │  ← private constructor
│ + getInstance() : Singleton [static]│  ← global access point
│ + businessMethod() : void           │
└─────────────────────────────────────┘

        Client ───uses───► Singleton.getInstance()
                                │
                                ▼
                    ┌───────────────────┐
                    │  Single Instance  │
                    │  (heap memory)    │
                    └───────────────────┘

Key relationships:

  • The class holds a private static reference to itself.
  • The constructor is private - no new Singleton() from outside.
  • getInstance() is the only creation path.

Pseudocode Implementation

Variant 1: Basic Singleton (Not Thread-Safe)

public class Singleton {
    private static Singleton instance;

    private Singleton() {}  // private constructor

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();  // race condition here
        }
        return instance;
    }
}

Pros: Simple, lazy initialization. Cons: Broken in multithreaded environments - two threads can create two instances.


Variant 2: Thread-Safe with Synchronized Method

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Pros: Thread-safe, lazy. Cons: synchronized on every call - severe performance penalty (100x slower) even after instance exists.


Variant 3: Double-Checked Locking (DCL)

public class Singleton {
    private static volatile Singleton instance;  // volatile is critical

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                // 1st check (no lock)
            synchronized (Singleton.class) {
                if (instance == null) {        // 2nd check (with lock)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Pros: Thread-safe, lazy, synchronization cost only on first creation. Cons: Requires volatile (Java 5+); subtle - easy to implement incorrectly. Without volatile, instruction reordering can expose a partially constructed object.


Variant 4: Bill Pugh Singleton (Static Inner Class)

public class Singleton {
    private Singleton() {}

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

Pros: Lazy (inner class loaded only on first getInstance() call), thread-safe (JVM classloader guarantees), no synchronization overhead, no volatile needed. Cons: Cannot pass construction parameters. Still vulnerable to reflection attacks.

This is the recommended approach for most Java applications.


Variant 5: Enum-Based Singleton

public enum Singleton {
    INSTANCE;

    public void businessMethod() {
        // implementation
    }
}

// Usage: Singleton.INSTANCE.businessMethod();

Pros: Thread-safe, serialization-safe (JVM handles it), reflection-proof (JVM prevents enum constructor invocation via reflection), concise. Cons: Cannot extend other classes (enums implicitly extend java.lang.Enum), eager initialization, feels unconventional to some developers.

Joshua Bloch (Effective Java): "A single-element enum type is the best way to implement a singleton."


Variant Comparison Summary

VariantThread-SafeLazyReflection-ProofSerialization-SafePerformance
BasicFast
SynchronizedSlow
Double-Checked LockingFast (after init)
Bill PughFast
Enum(eager)Fast

Real-World Examples

1. Logger (java.util.logging.Logger)

All modules obtain the same Logger instance for a given name. Ensures consistent log formatting, destination, and level configuration.

2. Configuration Manager

AppConfig.getInstance().getProperty("db.url")

Reads config once from file/environment, serves all components. Avoids repeated I/O and inconsistent snapshots.

3. Database Connection Pool (HikariCP, C3P0)

A single pool manages a fixed number of connections. Multiple pools would exhaust database connection limits and waste memory.

4. Thread Pool (Executors)

Runtime.getRuntime() - the JVM Runtime is a Singleton. Thread pools are typically singleton-scoped to control concurrency limits globally.

5. Spring Framework Beans

Spring's default scope is singleton - one bean instance per application context. The container manages lifecycle, not the class itself.

6. Java Runtime

Runtime.getRuntime() is a classic JDK Singleton. There's exactly one Runtime per JVM process, providing access to memory info, process execution, and shutdown hooks.

7. Desktop Applications

Print spoolers, clipboard managers, and window managers in GUI frameworks are typically singletons - they coordinate access to a single physical or OS-level resource.

Implementation Pattern in Real Systems

// How HikariCP effectively works as a singleton pool
public class DataSourceProvider {
    private static class Holder {
        static final HikariDataSource DS = createDataSource();
        private static HikariDataSource createDataSource() {
            HikariConfig config = new HikariConfig("/hikari.properties");
            return new HikariDataSource(config);
        }
    }
    public static DataSource getDataSource() {
        return Holder.DS;
    }
}

Singleton vs Static Class

CriteriaSingletonStatic Class
InstanceHas an actual object on the heapNo instance; just static methods
Interface ImplementationCan implement interfacesCannot
PolymorphismSupportsNot possible
Lazy LoadingControlledLoaded at class-load time
SerializationPossible (with care)Not applicable
Testability (Mocking)Possible via interface extractionVery difficult to mock
StateCan hold instance stateOnly static state
InheritanceCan extend classesCannot
Dependency InjectionCompatibleNot injectable
MemoryGC can collect (if reference lost)Lives until classloader unloads

Guideline: Use a static class for pure utility functions (e.g., Math, StringUtils). Use Singleton when you need state, polymorphism, or lifecycle control.


Advantages & Disadvantages

AdvantagesDisadvantages
Controlled access to sole instanceViolates Single Responsibility Principle (manages own lifecycle + business logic)
Reduced memory footprint (one object)Introduces global state - hidden dependencies
Lazy initialization saves startup timeTestability nightmare - hard to mock, hard to reset between tests
Can be extended via subclassing (with care)Tight coupling - clients depend on concrete class
Thread-safe access to shared resourceConcurrency complexity in implementation
Better than global variables (encapsulated)Violates Dependency Inversion Principle
Compatible with interfaces for abstractionCan mask poor architecture (God object smell)

Constraints & Edge Cases

1. Multithreading

Without proper synchronization, race conditions create multiple instances. DCL requires volatile; Bill Pugh and Enum are inherently safe.

2. Serialization / Deserialization

Default deserialization creates a new object via the ObjectInputStream bypassing the constructor entirely. This means even a private constructor won't protect you. Fix:

// Add this method to your Singleton class
protected Object readResolve() {
    return getInstance();  // discard deserialized object, return existing
}

Additionally, implement Serializable carefully - all fields should be transient or the deserialized state could conflict with the live instance's state.

3. Reflection Attack

Constructor<Singleton> c = Singleton.class.getDeclaredConstructor();
c.setAccessible(true);
Singleton second = c.newInstance();  // breaks singleton!

Defense strategies:

private Singleton() {
    if (instance != null) {
        throw new IllegalStateException("Instance already created. Use getInstance().");
    }
}

The Enum approach is the only implementation that is completely immune - the JVM itself throws IllegalArgumentException if you attempt to reflectively instantiate an enum.

4. Classloader Issues

Different classloaders can load the same class independently, each creating its own Singleton. Relevant in application servers (EAR with multiple WARs). Solution: Use a common parent classloader or framework-managed singletons.

5. Garbage Collection

If no live reference to the Singleton exists, the GC can collect it (pre-Java 1.2 was aggressive about this). Modern JVMs with static references prevent this, but in OSGi/modular environments, bundle unloading can destroy singletons.

6. Cloning

Override clone():

@Override
protected Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException("Singleton cannot be cloned");
}

Anti-Pattern Discussion

Why Singleton Is Often Considered an Anti-Pattern

  1. Hidden Dependencies - Code that calls Singleton.getInstance() has an invisible dependency not expressed in its constructor or method signature.
  2. Global Mutable State - Essentially a glorified global variable; makes reasoning about program state difficult.
  3. Testing Pain - Cannot easily substitute a mock; tests become order-dependent if the Singleton carries state between tests.
  4. Concurrency Bugs - Shared mutable state is the root of most concurrency issues.
  5. Violation of SOLID - Breaks SRP (two responsibilities) and DIP (depends on concrete class).

Alternatives

AlternativeHow It Helps
Dependency Injection (DI)Framework manages single-instance lifecycle; class is unaware it's a singleton
Factory + Scope ManagementFactory decides how many instances; class remains reusable
Service LocatorCentralized registry without hardcoding getInstance() calls
Module-level instanceIn languages like Python/JS, module itself is a singleton scope

Modern best practice: Let a DI container (Spring, Guice, Dagger) manage singleton scope. The class itself should be a plain POJO with constructor injection.

The Testing Problem in Detail

// This is UNTESTABLE in isolation:
public class OrderService {
    public void placeOrder(Order order) {
        DatabasePool.getInstance().getConnection();  // hidden dependency
        Logger.getInstance().log("Order placed");    // hidden dependency
    }
}

// This is TESTABLE:
public class OrderService {
    private final DataSource dataSource;
    private final Logger logger;

    public OrderService(DataSource ds, Logger logger) {  // injected
        this.dataSource = ds;
        this.logger = logger;
    }
}

The first version cannot be unit-tested without a real database and real logger. The second version accepts mocks. This is the fundamental argument against Singleton in modern software - it couples your code to a specific instance strategy rather than an abstraction.


Interview Follow-ups

Q1: How can you break a Singleton in Java?

Answer: There are four ways to break a Singleton:

  1. Reflection - Access private constructor via setAccessible(true).
  2. Serialization - Deserialize creates a new object (fix with readResolve()).
  3. Cloning - If class implements Cloneable (fix by overriding clone()).
  4. Multiple Classloaders - Each classloader creates its own instance.

The Enum-based Singleton is immune to reflection and serialization attacks.

Q2: How do you make a Singleton thread-safe?

Answer: Multiple approaches in order of recommendation:

  1. Enum Singleton - JVM guarantees thread safety.
  2. Bill Pugh (static inner class) - Classloader guarantees; no synchronization cost.
  3. Double-Checked Locking - volatile + synchronized block; lock only on first creation.
  4. Synchronized getInstance() - Correct but slow; lock on every access.
  5. Eager initialization - Create at class-load time; JVM handles thread safety.

Q3: Singleton vs Spring Singleton Scope - what's the difference?

Answer: A GoF Singleton enforces one instance per JVM via private constructor. A Spring singleton bean is one instance per ApplicationContext - the class itself is a normal class with public constructor. Multiple contexts (e.g., in tests) can create multiple instances. Spring manages the lifecycle externally; the class has no singleton logic.


Hint-Only Questions (for self-practice)

Q4: How would you implement a Singleton in a distributed system (multiple JVMs)? Hints: Think about distributed caches (Redis, Hazelcast), database locks, or ZooKeeper-based leader election. A JVM-level Singleton doesn't help across nodes.

Q5: Can you make a Singleton that accepts constructor parameters? Hints: Consider a getInstance(Config config) that only uses the parameter on first call. Or use a separate initialize() method. Think about why this is a design smell - if it needs parameters, is it really a Singleton or a managed dependency?


Counter Questions to Ask Interviewer

Use these to demonstrate depth and steer the conversation:

  1. "Should the Singleton be managed by the class itself or by a DI container?" - Shows awareness of modern practices vs. textbook patterns.
  2. "Is this Singleton expected to work across multiple classloaders or a single application context?" - Demonstrates understanding of enterprise edge cases.
  3. "Does the system require the Singleton to survive serialization (e.g., distributed cache, session replication)?" - Shows you think about real deployment scenarios.
  4. "Are there testability requirements? Should we be able to substitute this in unit tests?" - Signals you value clean architecture over pattern dogma.
  5. "Is lazy initialization important here, or is eager acceptable given the startup cost?" - Shows you weigh tradeoffs rather than defaulting to the most complex solution.

References & Whitepapers

  1. Gamma, Helm, Johnson, Vlissides - Design Patterns: Elements of Reusable Object-Oriented Software (GoF, 1994), Chapter 3: Creational Patterns.
  2. Joshua Bloch - Effective Java, 3rd Edition (2018), Item 3: "Enforce the singleton property with a private constructor or an enum type."
  3. Brian Goetz - Java Concurrency in Practice (2006), Section 16.2: Safe Publication and Double-Checked Locking.
  4. Steve Yegge - "Singletons Considered Stupid" (2004) - Critique of Singleton overuse.
  5. Mark Radford - "Singleton: the anti-pattern" (Overload Journal, 2003).
  6. Martin Fowler - Inversion of Control Containers and the Dependency Injection Pattern (2004).
  7. Java Language Specification - §12.4: Initialization of Classes and Interfaces (guarantees thread-safe class loading).


Singleton is the simplest pattern to describe and the hardest to justify in modern codebases. Know it deeply for interviews - then use DI in production.