Cruxly

Cruxly › Low-level design › More Design Patterns

Singleton

exactly one instance, reachable from everywhere — and why that is usually a trap

Open the interactive lesson

Forcing problem: something in the system must have exactly one instance — a connection pool, a hardware resource, a single source of configuration — and many unrelated parts of the code need to reach it.

This is the pattern most often reached for reflexively and most often the wrong answer. Both halves matter here: how to build it correctly, and when not to.

The naive version, and the concurrency bug inside it

class ConfigRegistry {
    private static ConfigRegistry instance;

    private ConfigRegistry() { /* expensive: reads files, parses */ }

    static ConfigRegistry getInstance() {
        if (instance == null) {              // (A) check
            instance = new ConfigRegistry();  // (B) act
        }
        return instance;
    }
}

Two threads calling getInstance() for the first time can both pass check (A) before either completes (B) — the identical check-then-act race covered in the rate limiter and the parking lot elsewhere in this track, just relocated to object construction. Both threads construct a ConfigRegistry, one is silently discarded, and if the constructor has side effects (opening a file handle, registering with something external) those side effects now happen twice.

Three correct fixes, and what each costs

1. Eager initialization. Construct at class-load time, which the JVM already guarantees is thread-safe and happens at most once.

class ConfigRegistry {
    private static final ConfigRegistry INSTANCE = new ConfigRegistry();
    private ConfigRegistry() { }
    static ConfigRegistry getInstance() { return INSTANCE; }
}

Simplest and safest. Costs: the instance is constructed even if the application never calls getInstance(), which matters if construction is genuinely expensive and sometimes unnecessary.

2. Double-checked locking, for genuinely lazy construction under concurrency:

class ConfigRegistry {
    private static volatile ConfigRegistry instance;   // volatile is not optional

    static ConfigRegistry getInstance() {
        ConfigRegistry local = instance;
        if (local == null) {
            synchronized (ConfigRegistry.class) {
                local = instance;
                if (local == null) {                    // check AGAIN inside the lock
                    instance = local = new ConfigRegistry();
                }
            }
        }
        return local;
    }
}

volatile is load-bearing, not decoration. Without it, another thread can observe a partially constructed object — the reference gets published before the constructor's writes are guaranteed visible, due to instruction reordering the JVM is otherwise permitted to do. This is the single most commonly broken detail when this pattern is implemented from memory rather than understood.

3. The initialization-on-demand holder — simplest correct lazy version, using the same class-loading guarantee as eager initialization but deferred until first use:

class ConfigRegistry {
    private ConfigRegistry() { }
    private static class Holder {                        // loaded only on first access
        static final ConfigRegistry INSTANCE = new ConfigRegistry();
    }
    static ConfigRegistry getInstance() { return Holder.INSTANCE; }
}

No synchronized, no volatile, and correctly lazy — the JVM does not load Holder until getInstance() first references it, and class loading is already thread-safe. This is the version to reach for when lazy construction is genuinely needed and no framework is providing the lifecycle.

The mechanism: what the JVM actually guarantees

naive: two threads race T1: instance == null? yes T2: instance == null? yes (before T1 writes) T1 constructs #1 T2 constructs #2 two instances exist holder: JVM serializes loading class-loading lock: both threads enter here T1: constructs, holds lock T2: waits for lock exactly one instance
The naive version's race lives entirely in application code. The holder pattern moves the synchronization into the JVM's own class-loading mechanism, which was already required to be correct.

Why this pattern is usually the wrong answer anyway

Global mutable state is exactly what makes tests slow and order-dependent. A Singleton holding state that tests read or modify means test order starts to matter, and parallel test execution becomes unsafe unless the Singleton is reset between tests — a maintenance burden the pattern itself creates.

"Exactly one instance" and "globally reachable from anywhere" are two separate requirements, and Singleton conflates them. Dependency Injection, covered elsewhere in this track, gets you the first without the second: construct the ConfigRegistry once, at the composition root, and hand it to whatever needs it via the constructor. This preserves "exactly one instance exists" while keeping the object out of static global scope — callers depend on an interface ConfigRegistry provides, not on the pattern's own getInstance() static access point, so a test can substitute a fake without any of the plumbing above.

The honest test for when Singleton earns its place: the "one instance" constraint is enforced by something outside your control — a hardware resource, an OS-level handle, a genuinely process-wide invariant — rather than merely being a design preference for convenience. A single database connection pool your application chooses to share is DI. A singleton wrapping the one physical GPU on the machine is a real Singleton.

:::warn The interview tell "I'd make it a Singleton" as a first instinct, for anything that merely needs to be shared, is the sign to watch for. The stronger answer is "this needs exactly one instance — I'd construct it once at the composition root and inject it, rather than reaching for a static accessor," which gets the actual requirement without the testability cost. :::

How to say it in the interview

"Before writing a Singleton I ask whether this really needs to be globally reachable, or whether it just needs to be constructed once and then handed to the things that use it — because those are different problems, and only the first one is actually Singleton."

Checklist

Practice this interactively
Share this lessonWhatsAppLinkedInX

Related lessons