Creational · 5 of 5 · use rarely
Singleton — and when not to
means exactly one instance in the process, with a global way to get it. In most interview designs that is the wrong default. Prefer injecting collaborators so each test can supply its own.
Clock.getInstance(), TicketRepository.getInstance(), and ParkingLot.getInstance() look tidy — but tests cannot freeze time, two test methods share tickets, and a second gate cannot have its own lot. is usually a global variable with a private constructor.
Is this a true process-wide resource — and can a test replace it?
Avoid
- Clock.getInstance()
- Lot.getInstance()
- TicketRepository.getInstance()
- Any collaborator a test must replace
Prefer
- ParkingLot(Clock clock, …)
- new ParkingLot(...) in main / test
- Explicit collaborator, not a hidden global
- See dependency injection
Prefer injection (what interviews reward)
class ParkingLot {
private final Clock clock; // java.time.Clock
private final TicketRepository tickets;
ParkingLot(Clock clock, TicketRepository tickets) {
this.clock = clock;
this.tickets = tickets;
}
Instant issuedAt() { return clock.instant(); }
}
// production
new ParkingLot(Clock.systemUTC(), new InMemoryTickets());
// test
new ParkingLot(
Clock.fixed(Instant.parse("2026-08-13T10:00:00Z"), ZoneOffset.UTC),
new InMemoryTickets());If you truly need one instance, skip getInstance() gymnastics
An is the least-bad Java singleton. It is still a global. Prefer inject.
// Least-bad Java singleton — still a global. Prefer inject.
enum CountryCodes {
INSTANCE;
private final Map<String, String> byIso = Map.of("IN", "India");
String name(String iso) { return byIso.get(iso); }
}When not to use it
Anything a test must replace
Clock, random, HTTP, database. If park() reads Instant.now(), a two-hour stay is not a unit test. Inject Clock.
Mutable state more than one test will see
“There is only one parking lot in the problem” is not a reason — the next sentence is two lots, or a unit test that builds a lot per method.
Config, loggers, caches as singletons
Every class can reach them, so every class depends on them. That is hidden coupling, the opposite of DIP.
Factory or registry dressed as Singleton
A factory can be an ordinary object you inject. A registry can be a map you construct in tests with two handlers. Do not hide those behind getInstance() just because “there is only one in production.”
When it might be honest
A process-wide, immutable lookup loaded once at startup (country codes), or a genuine native resource you cannot have two of. Even then, construct it once in the and pass it in. You get “one instance” without getInstance() and without blocking tests.
- Ask if tests need a substitute. If yes, inject. Clock and stores almost always yes.
- Ask if two instances would be meaningless. Two lots are meaningful. Two process-wide country-code tables may not be.
- Wire once at the edge. main (or a test setup) constructs; everyone else receives.
Singleton for Clock, store, or the parking lot
Those are collaborators. Inject them. getInstance() makes time unfreezeable, tests share mutable tickets, and “add a second lot” becomes a rewrite.
Avoid double-checked locking theater
Interviewers have seen the broken version more than the correct one. If you insist on a lazy singleton, use an enum or a holder class. Better: construct once at startup and pass it in — no locking story required.
Singleton work in distributed systems is a different idea
System design talks about “singleton work” — one nightly report, one schema change — and uses leader election so only one machine does it. That is not this pattern. Do not put ParkingLot.getInstance() on the whiteboard because the HLD note said “singleton work.” In LLD, one lot is new ParkingLot(...) in main.
Lazy vs eager is the wrong debate
Prefer eager construction in main. You do not need a locking story. Lazy singletons mostly delay work that should not have been global. If startup cost is the worry, still construct the lookup in main and pass it in — one instance, still replaceable in tests. Lazy initialization plus locking is usually a distraction.
If the interviewer still wants the enum idiom, write it quickly and return to injection.
Two lots, two clocks, one process
The follow-up is almost always “now there are two lots” or “airport vs mall pricing.” getInstance() makes that a rewrite. Two ParkingLot instances in one JVM is normal. Two Clocks in one test suite is how you freeze Monday in one method and Tuesday in another. Singleton fights both.
If the interviewer says “there is only one lot in the problem,” answer: “I will still construct it in main so a test can build a smaller one.” That is the same move as testable design.
What to do if the interviewer asks for Singleton
Name the pattern. Then say why you would still inject. If they insist on the Java idiom, write the enum in thirty seconds and move on. Do not spend the round on volatile and double-checked locking.
// Holder idiom — if they want lazy, still prefer inject.
final class CountryCodesHolder {
private CountryCodesHolder() {}
private static class Holder {
static final CountryCodes INSTANCE = new CountryCodes();
}
static CountryCodes instance() { return Holder.INSTANCE; }
}Then immediately: “I would still construct this once in main and pass it in so tests can substitute a smaller table.” That follow-up keeps the design testable.
Hidden coupling in one picture
Every class can reach Clock
FeeEngine, Gate, Report, and a forgotten helper all call getInstance(). You cannot freeze time in one test without freezing it for all tests in the JVM.
The constructor lists friends
ParkingLot(Clock, Tickets, Allocation) is a document. A new collaborator is a new argument. That is the design, not a missing annotation.