Design Pattern

Singleton Pattern

Clean Java-only production-ready implementation.


What Problem It Solves

Ensure a class has exactly ONE instance and provide a global point of access.

Common Interview Traps

TrapWhyFix
Forgetting volatile in JavaA thread may see a partially constructed instanceUse volatile + double-checked locking, or the holder-pattern idiom
Creating a new instance in getInstance() every timeBreaks the singleton guaranteeUse static final field or __new__ override
Making singleton a God classSingleton has too many responsibilitiesSingleton only ensures single instance โ€” it's still SRP

Where Singleton Appears in LLD Problems

ProblemSingleton ClassWhy
Parking Lot (#1)ParkingSpotManagerCentral spot allocation across all entry gates
Logger (#15)LogManager / LoggerSingle log queue to avoid interleaving
Cache (#31)CacheManagerConsistent eviction state across consumers
Config (many)AppConfig / EnvironmentLoaded once from file/env
Notification (#18)NotificationQueueSingle queue of pending sends
Job Scheduler (#32)SchedulerOne scheduler loop managing all timers

How to Present in an Interview

"If this class is instantiated multiple times, we'd get [inconsistent state / duplicate threads / wasted memory]. I'll make it a singleton. For thread safety, I'll use double-checked locking with a volatile field (Java) or a module-level instance (Python). The class uses a private constructor so no one else can instantiate it."

When NOT to Use Singleton

  • Singleton makes unit testing harder (you can't inject a mock). If testability matters, pass instances via dependency injection instead.
  • If you need multiple instances later (multi-tenant config), Singleton is wrong.

Implementation Flavors

1. Eager Initialization (Simple, Safe)

Static initialization creates the instance immediately during class loading. It is simple, thread-safe, and perfect if the instance is cheap or always used. To be absolutely resilient against reflection/serialization attacks out of the box, use an **Enum Singleton**.

2. Lazy Initialization (Thread-Safe)

Use when initialization is expensive (reads from disk, connects to DB) and you may not always need it. For senior/production-ready applications, the implementation must prevent **Reflection attacks**, **Serialization bypasses**, and **Cloning bypasses**, while securing thread-safety via **Double-Checked Locking (DCL)** with a volatile field, or the static **Bill Pugh Holder Idiom**.

// โ”€โ”€โ”€ DOUBLE-CHECKED LOCKING SINGLETON (SENIOR PATTERN) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Thread-safe, high-performance, and resilient against typical bypass vectors:
// 1. Reflection Attacks (guards in constructor)
// 2. Serialization Attacks (defines readResolve())
// 3. Cloning Attacks (overrides clone())
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import java.io.Serializable;

final class DatabaseConnectionPool implements Serializable, Cloneable {
    private static final long serialVersionUID = 1L;
    
    // volatile is CRITICAL: prevents instruction reordering.
    // Without volatile, a thread may observe a non-null, uninitialized reference:
    // Memory Allocated -> Assigned to Reference -> Constructor Executes (Reordered!)
    private static volatile DatabaseConnectionPool instance;

    private DatabaseConnectionPool() {
        // Reflection defense: prevent instantiation via Constructor.newInstance()
        if (instance != null) {
            throw new IllegalStateException("Instance already initialized. Use getInstance().");
        }
        System.out.println("  [DatabaseConnectionPool] Expensive resource initialization...");
    }

    public static DatabaseConnectionPool getInstance() {
        DatabaseConnectionPool localRef = instance; // Use local variable for 25% performance boost
        if (localRef == null) {
            synchronized (DatabaseConnectionPool.class) {
                localRef = instance;
                if (localRef == null) {
                    instance = localRef = new DatabaseConnectionPool();
                }
            }
        }
        return localRef;
    }

    // Serialization defense: returns the existing instance during deserialization
    protected Object readResolve() {
        return getInstance();
    }

    // Cloning defense: prevent cloning
    @Override
    protected Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException("Singleton cloning is prohibited.");
    }
}

public class Main {
    public static void main(String[] args) {
        DatabaseConnectionPool pool1 = DatabaseConnectionPool.getInstance();
        DatabaseConnectionPool pool2 = DatabaseConnectionPool.getInstance();
        
        System.out.println("  [Main] Same instance? " + (pool1 == pool2)); // Should output: true
    }
}

๐Ÿ’ฌReview

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...