How would you implement a singleton, and why do many teams treat it as an anti-pattern?
The mechanics are easy - an enum in Java, a module-level value elsewhere - and are not the interesting part. The objection is that a singleton bundles two unrelated decisions, one instance and globally reachable, and the second is what makes dependencies invisible and tests order-dependent.
What the interviewer is scoring
- Whether the candidate separates single-instance from globally-accessible as distinct decisions
- That the testing objection is stated as hidden shared state rather than just "hard to mock"
- Does the candidate know a correct lazy thread-safe implementation and why the naive one fails
- Whether they recognise the container-managed singleton as the same lifetime without the global access
- That a legitimate use is named rather than rejecting the pattern outright
Answer
Two decisions wearing one name
A singleton makes two commitments that have nothing to do with each other, and almost all the trouble comes from the second.
The first is one instance: this thing should exist once in the process. That is often a perfectly reasonable statement about a connection pool or a metrics registry, and nobody objects to it.
The second is globally reachable: any code anywhere may obtain it by naming the class. This is the one that causes damage, and the reason it hides is that the pattern bundles them, so accepting the sensible constraint appears to require accepting the harmful one.
Separate them and the objection resolves. Create the object once, at the top of the program, and pass it to whoever needs it. You have kept the lifetime guarantee and discarded the global access, and none of the problems below arise.
What the global access costs
A class that calls Config.getInstance() inside a method has a dependency that does not appear in its constructor, its signature, or anything a reader can see without opening the body. You cannot tell what a class needs by looking at how it is built, which is precisely the information a reader most wants.
In tests this stops being an aesthetic complaint. Because the instance outlives any one test, state written by one test is visible to the next, so the suite passes in the order it was written and fails when the runner reorders or parallelises it. The resulting flakiness is genuinely hard to attribute, because the failing test is not the one that caused it. Teams then add reset hooks between tests, which is a maintenance burden that exists purely to undo the pattern.
Initialisation order is the other recurring failure. A singleton initialised on first access is initialised at a time determined by whoever touches it first, which may be a static initialiser, a test harness, or a background thread. If that construction reads configuration or opens a connection, the behaviour depends on call order that nobody wrote down.
Implementing it properly, if you must
In Java the correct answer is an enum. It is a single instance guaranteed by the JVM, thread-safe initialisation for free, and it is the one form that survives serialisation and reflection without extra work.
public enum ConnectionPool {
INSTANCE;
// Runs once, at class initialisation, with the JVM guaranteeing
// that no other thread sees a partially constructed value.
private final DataSource dataSource = buildDataSource();
public DataSource dataSource() { return dataSource; }
}
If the instance must be created lazily because construction is expensive and may not be needed, the holder idiom is the clean way. A nested class is not initialised until first referenced, and the JVM's class-initialisation lock does the synchronisation, so there is no lock in the fast path and no memory-visibility question to reason about.
public final class ConnectionPool {
private ConnectionPool() {}
private static class Holder {
// Not initialised until getInstance() first touches Holder.
static final ConnectionPool INSTANCE = new ConnectionPool();
}
public static ConnectionPool getInstance() { return Holder.INSTANCE; }
}
Both of these exist because the obvious lazy version is wrong. Checking for null, then constructing, is a race: two threads can both see null and both construct. Adding a lock around the whole method is correct but contends on every read. Double-checked locking — check, lock, check again — was famously broken before Java 5 and is only correct now if the field is volatile, because without it another thread can observe a non-null reference to an object whose constructor has not finished. If a candidate offers double-checked locking, the volatile is the whole answer, and the holder idiom avoids needing to explain any of it.
The version that is fine
A framework-managed singleton is a different animal despite the shared name. A Spring bean with singleton scope is created once per container and injected into the classes that need it. The lifetime guarantee is identical; the global access point is gone. Dependencies are visible in the constructor, a test can supply a different instance without touching global state, and a second container in the same process gets its own — which is what makes parallel integration tests possible.
That distinction is the practical resolution of the whole debate. Nobody objects to one instance. What they object to is getInstance().
Where it is genuinely right
Cross-cutting infrastructure that must be reachable from code you cannot thread a parameter into: a logging facade, a metrics registry, a runtime's scheduler. Threading a logger through every constructor in a large system is a real cost, and the state involved is usually append-only, so the test-pollution objection mostly does not apply.
The distinction worth drawing is between a singleton holding behaviour and one holding mutable domain state. A logger that everything can reach is a pragmatic trade. A globally reachable mutable cache of the current user is a bug that has not happened yet.
Nobody minds one instance. The problem is the static accessor, which hides the dependency from readers and shares state between tests that should not be able to see each other.
Likely follow-ups
- Why is double-checked locking broken without a memory barrier, and what fixes it?
- Your singleton reads configuration at class-initialisation time. What breaks in a test suite?
- How is a Spring singleton bean different from a classic singleton?
- When is a genuinely global single instance the right answer?
Related questions
- When is a factory the right answer, when is a builder, and when should you just call the constructor?mediumAlso on creational-patterns and singleton7 min
- What is the dependency inversion principle, and what exactly is being inverted?mediumAlso on testability3 min
- The business insists on a requirement that nobody can test — "the system must be intuitive". What do you write instead?mediumAlso on testability4 min
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?hardSame kind of round: concept7 min
- How do you test a service that calls a third-party API, without calling it?mediumSame kind of round: concept4 min
- You run ten coroutines concurrently and one of them raises. What happens to the other nine?hardSame kind of round: concept5 min
- Several producers write to one channel and one consumer reads it. Who closes the channel?mediumSame kind of round: concept4 min
- How does your order placement change on a venue that allocates pro rata within a price level instead of first in, first out?hardSame kind of round: concept6 min