beginner~2h

Singleton Pattern

Learn how to guarantee a class has exactly one instance application-wide, why the naive version breaks under concurrency, and why Singleton has a genuinely bad reputation despite being useful.

Learning objectives

  • Explain why an ordinary public constructor cannot guarantee a single shared instance
  • Build a Singleton using a private constructor, a static field, and a static accessor method
  • Fix the naive Singleton's race condition using double-checked locking
  • Name at least two ways a correctly implemented Singleton can still end up with more than one instance
  • Explain why Singleton is considered an anti-pattern in many modern codebases, and what a better alternative looks like

◆ Story

A country has exactly one currently-recognized government at any given time — not because it is technically impossible to declare a second one, but because having two would create genuine chaos: which one's laws actually apply? Some things in a system genuinely need to be singular, with every part of the system agreeing on the exact same one.

An application's settings are a good example of the same requirement. An AppSettings class holds configuration — a database URL, an API key — that every part of the application reads. There should only ever be one set of settings in memory at a time; two different parts of the running app disagreeing about the current API key is a real, live bug, not just untidiness.

Java gives every ordinary class a public constructor by default, and a public constructor means anyone, anywhere, can call new on it as many times as they like. Nothing about that default behavior expresses "there should only ever be one of these" — that guarantee has to be built deliberately.

The obvious first version of AppSettings is a completely ordinary class: a public constructor that reads configuration and sets a couple of fields.

Constructing it twice produces two separate objects. settings == settingsCopy prints false — genuinely different objects in memory, each holding its own independently-allocated copy of the same data. Right now, both happen to hold identical values, since both constructors read the same hardcoded configuration.

That is fine, as long as nothing ever changes after construction. The real danger appears the moment one part of the application updates its own instance's API key — imagine a hypothetical setter — expecting that change to be visible everywhere. It will not be. Every other part of the app holding a different instance never sees that update, and two parts of the same running application end up silently disagreeing about current configuration. If AppSettings held something genuinely heavy, like a live database connection or a large in-memory cache, every accidental extra instance would also duplicate that real resource cost for no reason.

The class's own name is a promise nothing in its code actually enforces: "app settings," singular. new AppSettings() behaves exactly like new on any other class — nothing about this specific class's meaning is reflected in what the compiler allows.

💻 Code example

class AppSettings { private String databaseUrl; private String apiKey; public AppSettings() { // public -- anyone, anywhere, can call new AppSettings() this.databaseUrl = "jdbc:postgresql://prod-db"; // imagine this read from a config file instead this.apiKey = "abc123"; } String getApiKey() { return apiKey; } } AppSettings settings = new AppSettings(); AppSettings settingsCopy = new AppSettings(); // a second, completely independent object System.out.println(settings == settingsCopy); // false -- two DIFFERENT objects in memory

The only thing that ever creates an object is its constructor, so if construction needs to be controlled, the constructor is exactly where that control has to live. If the constructor is made inaccessible from outside the class, nothing outside AppSettings can ever call new AppSettings() — not once. But something still has to hand callers an actual instance, so the class needs its own internal way to create exactly one, the first time it is asked, and remember it for every request after that.

That is the Singleton pattern: a private constructor, one private static field holding the single instance, and one public static method that creates it on first use and returns that same one forever after.

The static field belongs to the class itself, not to any one object — which matters, because before the first call, there is no object yet to hold it on. Sealing off the constructor with private is what actually enforces the guarantee: it makes new AppSettings() fail to compile from anywhere outside the class. The public static getInstance() method is the one sanctioned way in — it checks whether the instance field is still null, constructs the object only on that first call, and returns the same reference on every call after that.

getInstance() has to be static for the same reason the field does: a non-static method can only be called on an object that already exists, but the entire point of this method is to hand back an object when the caller might not have one yet. It has to be callable on the class itself, AppSettings.getInstance(), which is exactly what static provides.

💻 Code example

class AppSettings { private static AppSettings instance; // static -- belongs to the CLASS, not to any one object private String databaseUrl; private String apiKey; private AppSettings() { // PRIVATE -- new AppSettings() now fails to compile from outside this class this.databaseUrl = "jdbc:postgresql://prod-db"; this.apiKey = "abc123"; } public static AppSettings getInstance() { // static: called on the CLASS, no object exists yet if (instance == null) { // only true exactly once -- the very first call, ever instance = new AppSettings(); // the constructor CAN be called here -- we're inside the class } return instance; // every subsequent call just hands back the SAME object } String getApiKey() { return apiKey; } }

Calling getInstance() twice and comparing the results proves the guarantee: settings == settingsCopy now prints true. settingsCopy was never actually a copy at all — getInstance() handed back the exact same reference both times, exactly the test that failed with the naive public constructor.

There is a real bug hiding in this version, though, and it only shows up under concurrency. If two threads call getInstance() at nearly the same moment, both can potentially read instance == null as true before either one has finished constructing an object — and both proceed to construct and assign their own separate instance, silently breaking the "exactly one" guarantee this whole pattern exists to provide.

The standard fix is double-checked locking: check for null once, unsynchronized, as a fast path once the instance already exists; then, only if it looks null, acquire a lock and check again before actually constructing. The second check, taken inside the lock, is what closes the race the first check leaves open — it stops two threads that both passed the outer check simultaneously from each building their own object. The field also needs to be declared volatile: without it, a JVM optimization called instruction reordering could let one thread observe a partially-constructed object, with memory allocated but the constructor's field assignments not yet visible to that thread. A simpler, often-preferred alternative sidesteps all of this manual locking entirely: Java guarantees that an enum-based singleton is thread-safe automatically.

💻 Code example

public class Main { public static void main(String[] args) { AppSettings settings = AppSettings.getInstance(); AppSettings settingsCopy = AppSettings.getInstance(); System.out.println(settings == settingsCopy); // true -- the SAME object, both times System.out.println(settings.getApiKey()); } } // Thread-safe version, fixing the race in the lazy getInstance() above: class AppSettingsThreadSafe { private static volatile AppSettings instance; // volatile -- prevents a subtle reordering bug public static AppSettings getInstance() { if (instance == null) { // first check, unsynchronized -- fast path synchronized (AppSettingsThreadSafe.class) { if (instance == null) { // second check, INSIDE the lock -- closes the race instance = new AppSettings(); } } } return instance; } }

▲ Edge case: reflection can still break a Singleton

Java's reflection API can call a private constructor directly, bypassing getInstance() entirely and constructing a second instance anyway. A truly bulletproof singleton — specifically an enum-based one — is immune to this; a hand-rolled private-constructor version, on its own, is not.

▲ Edge case: serialization can also create a second instance

Deserializing a previously-serialized AppSettings object creates a brand-new instance by default, separate from whatever getInstance() currently returns — silently defeating the "exactly one" property the pattern exists to guarantee. Fixing this requires implementing readResolve() to explicitly return the existing singleton instance instead of the freshly deserialized one.

▲ Common mistake: reaching for Singleton by default

Singleton is simultaneously the most well-known and the most over-used creational pattern. A singleton is really just global, shared mutable state with a more respectable name, and it inherits all of global state's real problems: it is genuinely hard to unit test, since you cannot easily swap in a fake instance for a test when access is hardcoded to one static method; it hides a class's real dependencies, since any code can silently reach for AppSettings.getInstance() without that dependency ever appearing in a constructor signature; and it directly works against dependency inversion, since code depends on a concrete, globally-accessible instance instead of an injected abstraction.

In a Spring Boot application, a @Service-annotated class is a singleton-scoped bean by default — one shared instance, managed by the framework and injected wherever it is needed, rather than accessed through a global static method. That gives the genuine benefit of Singleton, exactly one shared instance, without its real cost, and is worth reaching for over a hand-rolled Singleton whenever a framework already offers it.

◆ Under the hood

Singleton shows up wherever a system needs one, and only one, shared instance of something, with every part of the application agreeing on it.

  • java.lang.RuntimeRuntime.getRuntime() returns the one instance representing the current JVM process, built with exactly the private-constructor-plus-static-accessor shape covered in this chapter.
  • Spring-managed beans — any @Component, @Service, or @Repository class is singleton-scoped by default, giving the whole application one shared, framework-managed instance without a hand-written getInstance() anywhere.
  • Logging frameworks — a single shared logger context per application, or per class, avoiding interleaved or duplicated writes to the same log destination.
  • Database connection pools — a pool object like a HikariCP DataSource, shared application-wide, coordinating a finite set of real connections from one place.
  • Caches — a single shared cache instance rather than several independent, and inconsistent, copies of the same cached data.

Q: What are the three essential pieces of a Singleton implementation? : A private static field to hold the one instance, a private constructor to block external construction, and a public static method that lazily creates the instance on first call and returns the same one on every call after.

Q: Why does the naive lazy-initialization Singleton break under concurrency? : Two threads can both read the instance field as null before either finishes constructing one, so both proceed to create separate objects — violating the "exactly one instance" guarantee the pattern exists to provide.

Q: Why does the double-checked locking implementation check for null twice? : The outer check avoids lock overhead once the instance already exists; the inner check, inside the lock, prevents two threads that both passed the outer check from each constructing a separate instance.

Q: What is the core, honest criticism of the Singleton pattern? : It is global mutable state — it hides a class's real dependencies and makes unit testing genuinely harder, directly conflicting with the Dependency Inversion Principle.

Q: Name two ways a "correctly" implemented Singleton can still end up with more than one instance. : Java's reflection API can invoke a private constructor directly, and deserializing a previously-serialized instance creates a new object by default — both bypass getInstance() entirely unless specifically guarded against.

Want a visual for this concept?

Generate a diagram tailored to “Singleton Pattern” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Factory Method Pattern← Back to all Low-Level Design & Design Patterns chapters