🏛️ Design Principles & Patterns · Advanced

Singleton in Java

Lazy holder, enum singleton, double-checked locking with volatile, why it hurts testing.

🧩 The mysteryYour tests pass one by one, then fail when run together. The culprit is a class that promised there would only ever be one of it.

Exactly one instance

A Singleton guarantees a single instance with a global access point. The private constructor stops anyone else calling new. Effective Java's favourite form is an enum with one constant: the JVM guarantees exactly one instance, even against serialization and reflection.

enum Config {
    INSTANCE;
    String url() { return "db://main"; }
}
// use: Config.INSTANCE.url()

Why enums beat classic singletons

A normal singleton has two back doors. Deserializing it can create a second instance, and reflection can call the private constructor. Enum constants are protected from both by the language itself, which is why enum Singleton { INSTANCE; } is the safest form.

The lazy holder idiom

Need lazy creation? The JVM initializes a class on its first active use, exactly once, even with racing threads. Holder isn't touched until get() runs, so the instance is created on demand. get() just reads a static final field: **no synchronized needed**.

class Config {
    private Config() {}
    private static class Holder {
        static final Config I = new Config();
    }
    static Config get() { return Holder.I; }
}
🔮 Predict it

When does the holder wake up?

What does this print?

class Box {
    private Box() { IO.println("built"); }
    static class Holder {
        static final Box I = new Box();
    }
    static Box get() { return Holder.I; }
}
void main() {
    IO.println("start"); Box.get(); Box.get();
}
  1. start built
  2. built start
  3. start built built
Show the answer

Nothing is built until the first get() touches Holder, so "start" comes first. Class initialization happens once, so the second get() reuses the same instance. Note: calling some *other* static method of Box would initialize Box only; Holder still waits for get(). That's what makes the idiom lazy.

⚠️ The trap

Broken double-checked locking

This famous idiom is broken because the inst field isn't volatile. The write of the reference can become visible to another thread before the constructor's writes, so that thread may see a half-built object. Fix: private static volatile Registry inst;

private static Registry inst;
static Registry get() {
    if (inst == null) {
        synchronized (Registry.class) {
            if (inst == null)
                inst = new Registry();
        }
    }
    return inst;
}

Singletons and tests

✗ Hidden global
class PriceService {
    int price(String sku) {
        return Cache.getInstance().get(sku);
    }
}

The cache lives for the whole JVM, so one test's data leaks into the next, and you can't swap in a fake.

✓ Injected instance
class PriceService {
    private final Cache cache;
    PriceService(Cache cache) {
        this.cache = cache;
    }
}

Production passes the one shared cache; each test passes a fresh fake.

💼 In the real world

On the job

Hidden singletons cause the classic "passes alone, fails in CI" test. Frameworks like Spring keep one shared instance per bean by default, but inject it rather than calling getInstance(). You get "only one" without the global-state pain.

Key takeaways

  1. enum Config { INSTANCE } is safe against serialization and reflection
  2. Holder idiom: lazy and thread-safe through class initialization
  3. Double-checked locking needs a volatile field
  4. Prefer injecting one shared instance over calling getInstance()

💡 A country has one official mint; everyone uses it, but that makes it hard to test with play money.

🤯 Did you know?

Double-checked locking was famously broken in Java until Java 5, when the revised memory model (JSR 133) gave volatile the guarantees that make it safe.

Practice questions

What does this print?

class Lazy {
    static { IO.println("outer"); }
    private static class H {
        static { IO.println("inner"); }
        static final Lazy I = new Lazy();
    }
    static Lazy get() { return H.I; }
    static void hi() { IO.println("hi"); }
}
// main runs: Lazy.hi(); Lazy.get();
  1. outer inner hi
  2. hi outer inner
  3. outer hi inner
  4. inner outer hi
Check your answer

outer hi inner. A class is initialized on first active use. Lazy.hi() initializes Lazy only; the nested holder H is initialized later, on the first get(). That's what makes the holder idiom lazy.

Why does Effective Java recommend enum Singleton { INSTANCE; }?

  1. Enums are faster than classes
  2. Enum constants can be lazily subclassed
  3. The JVM guarantees exactly one instance, even against serialization and reflection
  4. Enums are automatically thread-local
Check your answer

The JVM guarantees exactly one instance, even against serialization and reflection. Deserializing a normal singleton can create a second instance, and reflection can call a private constructor. Enums are protected from both by the language.

Next: why does Java write List.of(1, 2) and Integer.valueOf(5) instead of new? Static factories have superpowers.