LLD Dojo

Patterns you will actually be asked for · chapter 27 of 33

Singleton, and why interviewers hope you avoid it

Chapter 3.9 · Part 3, Patterns you will actually be asked for · about 25 minutes

What you need before this chapter: Part 1 in full, especially enum (1.9). Part 2 in full, especially dependency inversion (2.8). Chapter 3.2, Factory, for static factory methods, which this chapter contrasts directly against enforcing one instance.

When you finish this chapter you will be able to:


1. The situation

A parking garage has several gates, each printing tickets, and no two tickets anywhere in the building may share a number. A static field on the class that prints tickets is the obvious place to keep count, because a static field belongs to the class, not to any one object built from it.

final class TicketPrinter {
    private static long nextTicketNumber = 1;

    long printTicket() {
        return nextTicketNumber++;
    }
}

2. Naive code that is fine

TicketPrinter gateA = new TicketPrinter();
TicketPrinter gateB = new TicketPrinter();

System.out.println("gateA: " + gateA.printTicket());
System.out.println("gateB: " + gateB.printTicket());
System.out.println("gateA: " + gateA.printTicket());
javac Step1.java
java Step1
gateA: 1
gateB: 2
gateA: 3

Two separate TicketPrinter objects, and the numbers still come out sequential with no repeats, because nextTicketNumber is shared by the whole class regardless of how many objects exist. This already works, and it is worth noticing exactly why: nothing about having two TicketPrinter instances threatened the one thing that actually matters, which is the counter.

3. A new requirement

A test wants to check what a printer does once ticket numbers pass 999, so it wants a TicketPrinter that starts counting from a chosen number instead of 1. The obvious, small change is to make the counter an instance field with a constructor argument.

final class TicketPrinter {
    private long nextTicketNumber;

    TicketPrinter(long startAt) {
        this.nextTicketNumber = startAt;
    }

    long printTicket() {
        return nextTicketNumber++;
    }
}
javac Step2.java
java Step2
gateA: 1
gateB: 1
gateA: 2

Read that output again. gateA and gateB both print ticket number 1. Nothing in TicketPrinter rejected this change, and nothing about the class said it was unsafe, because nothing about the class ever stated the actual rule: there must be exactly one counter, ever, for the life of the building. Section 1's version only worked because a static field happens to be shared. It was never a rule the type enforced, and a change that looked completely reasonable, in isolation, broke it without a single compiler warning.

4. The move

Say the one-instance rule directly, as part of the type itself, so a second TicketPrinter cannot be built at all rather than merely being discouraged.

enum TicketPrinter {
    INSTANCE;

    private final AtomicLong nextTicketNumber = new AtomicLong(1);

    long printTicket() {
        return nextTicketNumber.getAndIncrement();
    }
}
System.out.println("gateA: " + TicketPrinter.INSTANCE.printTicket());
System.out.println("gateB: " + TicketPrinter.INSTANCE.printTicket());
System.out.println("gateA: " + TicketPrinter.INSTANCE.printTicket());
javac Step3.java
java Step3
gateA: 1
gateB: 2
gateA: 3

Every gate reaches the counter through TicketPrinter.INSTANCE, and there is only ever one INSTANCE, for the entire life of the program. Try to build a second one anyway.

TicketPrinter second = new TicketPrinter();
javac Step4Bad.java
Step4Bad.java:16: error: enum classes may not be instantiated
        TicketPrinter second = new TicketPrinter();
                               ^
1 error

That is a compile error, not a rule a reviewer has to remember to enforce. AtomicLong also replaces the unsafe long from section 3, since two real gates now genuinely can call printTicket from two threads at once. This is the Singleton pattern: a type that makes "exactly one of these will ever exist" a fact the compiler checks, instead of a fact people have to remember.

5. What modern Java changes here

The classic form of this pattern is a private constructor plus a static getInstance() method that lazily builds and caches one object, usually guarded with synchronized so two threads racing to call it for the first time cannot both build one. An enum with one constant gets the same guarantee directly from the language: the class loader builds INSTANCE exactly once, and there is no second constructor to call by accident, since an enum's constructors are always private, whether or not you write the word. Effective Java recommends the enum form for exactly this reason, and it is close to the only shape worth writing today.

6. When naming it is wrong

corpus/logger's own design notes reject a logger built the classic way, with a shared getInstance() every caller reaches through, for three concrete reasons. It cannot be built twice with different collaborators, so two independent tests in the same run cannot each get their own clock to control time with. It hides a real dependency instead of naming it in a constructor signature, which is the same problem chapter 2.8 named directly: code that quietly reaches out to a global instead of asking for what it needs as an argument. And it cannot be swapped for a test double without a reflection trick that would end up testing the trick, not the logger.

The threshold: a type enforcing one instance of itself is right only when a second instance existing would itself be a bug, the way a second independent TicketPrinter genuinely would double-issue numbers. Most things that look like "there is only one of these" are really "the caller only ever builds one" — one clock, one logger, one configuration, built once at start-up and handed to whatever needs it. That is an ordinary constructor argument, not a rule the type should enforce on itself, and reaching for enum INSTANCE there buys a fact nobody can test around, for a problem a constructor parameter already solved.

Your turn

Turn this seat allocator into a singleton the same way TicketPrinter was turned into one, so two counters requesting a seat can never be handed the same number.

final class SeatAllocator {
    private int nextSeat = 1;
    int allocate() { return nextSeat++; }
}

The answer.

enum SeatAllocator {
    INSTANCE;

    private final AtomicInteger nextSeat = new AtomicInteger(1);

    int allocate() {
        return nextSeat.getAndIncrement();
    }
}
javac Step5.java
java Step5
counter A: 1
counter B: 2
counter A: 3

Going deeper

The classic getInstance() form looks equivalent to enum INSTANCE until something outside your own code goes looking for a second instance on purpose. Reflection is the first way in. It can call a private constructor directly, setAccessible and all.

Constructor<ClassicSingleton> ctor = ClassicSingleton.class.getDeclaredConstructor();
ctor.setAccessible(true);
ClassicSingleton b = ctor.newInstance();
javac Step6.java
java Step6
a == b: false

a, fetched through getInstance(), and b, built by calling the private constructor directly through reflection, are two different objects. Nothing about a private constructor stops reflection from calling it; private is a compiler-enforced convention, not a runtime lock. Try the same move against an enum.

Constructor<EnumSingleton> ctor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
ctor.setAccessible(true);
ctor.newInstance("SECOND", 1);
javac Step7.java
java Step7
rejected: Cannot reflectively create enum objects

This check lives inside Constructor.newInstance itself, in the JDK, and it fires regardless of setAccessible. An enum is the one kind of class reflection is specifically forbidden from instantiating a second time.

Serialisation is the second way in, and it is easy to miss because nothing about writing an object to a stream looks like it should create a new one. Serialise the classic singleton and read it back.

ClassicSingleton original = ClassicSingleton.getInstance();
// ... write original to a byte stream, then read it back as copy ...
System.out.println("original == copy: " + (original == copy));
javac Step8.java
java Step8
original == copy: false

ObjectInputStream.readObject() allocates a new object without calling any constructor at all, so copy is a second, independent ClassicSingleton, holding whatever state original had at the moment it was written. Fixing this needs an extra method, readResolve, written by hand, that returns the canonical instance instead of the one just allocated. Run the same experiment on the enum version, with no readResolve anywhere.

javac Step9.java
java Step9
original == copy: true

enum serialisation is specified to write only the constant's name and look it up again with Enum.valueOf on the way back, so identity survives automatically. Two completely different attacks, reflection and serialisation, and enum closes both for the same reason: the guarantee is enforced by the language itself, not by a convention the class's own author has to remember to defend.

None of this is the real reason interviewers wince at Singleton, though. The real reason is what section 6 already named: it is a global. Two tests in the same process that both touch TicketPrinter.INSTANCE now share state neither one owns, so the order the tests happen to run in can change which one passes. A dependency passed in through a constructor has no such problem, because each test builds its own.

Why this matters in an interview

An interviewer who lets you reach for Singleton without comment is testing whether you notice the trade-off yourself. The strong answer is not "I would make it an enum." It is naming, unprompted, that a shared instance makes tests depend on execution order, and asking whether the requirement actually needs one instance enforced by the type, or just one instance built once and handed around.


Next: chapter 3.10, Composite and Iterator: trees and traversal. This chapter's rule was about how many objects exist. The last chapter in this part is about objects that contain other objects of their own kind, and how to walk through them without the caller needing to know how deep they go.

← 3.8 Adapter and Facade: making other people's code fit · All chapters · 3.10 Composite and Iterator: trees and traversal →