You’ve probably felt this pain: a “configuration” object (or logger, metrics registry, feature-flag client, cache, etc.) quietly multiplies across your app. Everything still compiles, but behavior gets weird—one part reads updated config, another reads stale values, and a third writes to a different file because it constructed its own logger. When that happens, debugging turns into a scavenger hunt across constructors and dependency graphs.
When I truly need exactly one instance of something for the lifetime of a JVM (or for a class loader), I reach for the Singleton design pattern. The goal is simple: enforce a single instance and provide a global access point. The reality is not always simple, especially once you add concurrency, testing, serialization, reflection, containers, and multiple class loaders.
In this post I’ll show you the versions of Singleton I trust in modern Java (Java 21+), why some textbook variants break under load, how to avoid the most common traps, and how to decide when a Singleton is the right tool versus when dependency injection or plain object lifecycles are safer.
The Core Problem: “Only One Config Manager”
Imagine an application-wide configuration object:
- It loads values from
application.properties(or env vars) at startup. - It caches computed values (timeouts, feature toggles, derived URLs).
- It must be consistent everywhere.
If every component can do new AppConfig(...), you’ll end up with:
- multiple file reads at startup
- multiple caches that disagree
- inconsistent behavior between modules
A Singleton addresses this by enforcing three ideas:
1) A private constructor so nobody else can instantiate the class.
2) A private static field holding the single instance.
3) A public static accessor that returns the instance.
That’s the design intent. The details of “how and when do we create it” determine whether it behaves correctly under concurrency and in real deployments.
A Minimal Singleton (And Why I Rarely Ship It)
Here’s the simplest lazy Singleton. It compiles and “works” in single-threaded scenarios:
public final class AppConfig {
private static AppConfig instance;
private AppConfig() {
// Load config here
}
public static AppConfig getInstance() {
if (instance == null) {
instance = new AppConfig();
}
return instance;
}
}
The problem: it’s not thread-safe. Two threads can race, both see instance == null, and both create an instance. That breaks the “exactly one instance” promise.
If you’re thinking “my app starts once, and this is only called during startup,” you might get away with it—until someone calls it during parallel initialization, a scheduled task, or a test suite that runs things in parallel. In 2026, concurrency is the default more often than not (virtual threads, async HTTP clients, parallel test runners), so I treat this version as a teaching snippet, not production code.
The Versions of Singleton I Actually Trust
When you ask me “What Singleton should I use in Java?”, I typically recommend one of these (in this order):
1) Enum singleton (strongest against serialization/reflection issues)
2) Initialization-on-demand holder (lazy, fast, clean)
3) Eager initialization (simple, if cost is tiny)
Before we dive in, here’s the mental model I use:
- Correctness first: it must be one instance per class loader, even under concurrency.
- Lifecycle clarity: it should be obvious when it initializes and how it shuts down.
- Testability: it should be possible to test code that depends on it without turning the test suite into a minefield.
Option 1: Enum Singleton (My Default When I Need a True Singleton)
Enum singletons are concise and have strong guarantees in the JVM. They handle serialization correctly by design and resist many reflection tricks.
public enum AppConfig {
INSTANCE;
private final int httpTimeoutMillis;
AppConfig() {
// Load once
this.httpTimeoutMillis = Integer.parseInt(
System.getProperty("http.timeout.ms", "2000")
);
}
public int httpTimeoutMillis() {
return httpTimeoutMillis;
}
}
Usage:
public class Main {
public static void main(String[] args) {
int timeout = AppConfig.INSTANCE.httpTimeoutMillis();
System.out.println("Timeout: " + timeout);
}
}
When I choose enum singletons:
- I truly want exactly one instance per class loader.
- The singleton needs to be serialization-safe.
- I want the fewest moving parts.
Trade-off: some developers dislike the “it’s an enum” shape for non-enum concepts. I’m fine with that; correctness beats aesthetics.
A practical note: enum singletons can still contain mutable state (like caches). The enum protects the identity of the singleton, not the purity of its contents. If you put mutable global state inside it, you still inherit the typical test/lifecycle complexity.
Option 2: Initialization-on-Demand Holder (Best Classic Singleton)
This pattern relies on class initialization rules: the inner holder class isn’t initialized until it’s referenced, and class initialization is thread-safe.
public final class AppConfig {
private AppConfig() {
// Load config once
}
private static final class Holder {
private static final AppConfig INSTANCE = new AppConfig();
}
public static AppConfig getInstance() {
return Holder.INSTANCE;
}
}
Why I like it:
- Lazy without explicit synchronization
- Very fast after initialization
- Easy to read once you’ve seen it
If you only learn one non-enum singleton variant, I’d pick this one. It’s the sweet spot: minimal code, no tricky volatile, and correctness is delegated to the JVM’s class initialization guarantees.
Option 3: Eager Initialization (If Construction Is Cheap)
If construction is basically free (no I/O, no heavy parsing), eager initialization is the simplest reliable approach.
public final class MetricsRegistry {
private static final MetricsRegistry INSTANCE = new MetricsRegistry();
private MetricsRegistry() {}
public static MetricsRegistry getInstance() {
return INSTANCE;
}
}
This avoids all lazy-init complexity. The downside is startup cost and any ordering issues if the constructor touches other global state.
Eager initialization shines when:
- the singleton is lightweight and stateless
- you’re fine paying the cost at class-load time
- you want to minimize “magic” during runtime
Thread Safety: The Tempting Versions I Avoid
You’ll often see synchronized slapped onto getInstance(). It works, but it can add unnecessary lock overhead in hot paths.
public final class SlowButCorrectSingleton {
private static SlowButCorrectSingleton instance;
private SlowButCorrectSingleton() {}
public static synchronized SlowButCorrectSingleton getInstance() {
if (instance == null) {
instance = new SlowButCorrectSingleton();
}
return instance;
}
}
In many apps, the accessor isn’t called frequently enough for this to matter. But if getInstance() sits on a performance-sensitive path (logging adapters, metrics, tight loops), I prefer the holder pattern instead.
A nuance I care about: it’s not only the overhead, it’s the incentive. When getInstance() is cheap, people sprinkle it everywhere, and then it becomes hard to untangle hidden dependencies later. With synchronized access, you can accidentally convert that “harmless global” into a silent contention point.
Double-Checked Locking (Correct Only If You Do It Exactly Right)
Double-checked locking can be correct in modern Java, but only if the instance field is volatile.
public final class DclSingleton {
private static volatile DclSingleton instance;
private DclSingleton() {}
public static DclSingleton getInstance() {
DclSingleton local = instance;
if (local == null) {
synchronized (DclSingleton.class) {
local = instance;
if (local == null) {
local = new DclSingleton();
instance = local;
}
}
}
return local;
}
}
This version is fine, but it’s easier to get subtly wrong than the holder pattern, so I rarely pick it unless I’m working in a codebase that already standardizes on it.
If you want the deeper “why”: without volatile, the JVM is allowed to reorder certain writes, and another thread can observe a non-null reference to a partially constructed object. That’s the kind of bug that shows up once every few million requests—exactly the worst kind.
Singleton for Database Connection Management (And the Trap People Fall Into)
A common example is a “database connection manager.” The intent is usually:
- avoid creating many expensive resources
- share a central point for acquiring connections
Here’s the important distinction I want you to internalize:
- You almost never want a single JDBC
Connectionshared across a whole application. - You often do want a single
DataSource(connection pool) shared across the app.
A single Connection becomes a bottleneck, breaks transaction boundaries, and can fail in surprising ways under concurrency.
A Practical Singleton: One DataSource (Pool) for the JVM
This example uses a holder-based singleton around a DataSource. I’m intentionally keeping it runnable without external libraries by using a basic DriverManager-backed DataSource implementation, but in real services I’d use a pool (commonly HikariCP).
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public final class Database {
private final DataSource dataSource;
private Database() {
this.dataSource = new SimpleDriverManagerDataSource(
System.getProperty("db.url"),
System.getProperty("db.user"),
System.getProperty("db.pass")
);
}
private static final class Holder {
private static final Database INSTANCE = new Database();
}
public static Database getInstance() {
return Holder.INSTANCE;
}
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
// Minimal DataSource wrapper for demo purposes
private static final class SimpleDriverManagerDataSource implements DataSource {
private final String url;
private final String user;
private final String pass;
private SimpleDriverManagerDataSource(String url, String user, String pass) {
if (url == null || url.isBlank()) {
throw new IllegalArgumentException("db.url must be set");
}
this.url = url;
this.user = user;
this.pass = pass;
}
@Override
public Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, user, pass);
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return DriverManager.getConnection(url, username, password);
}
// The remaining DataSource methods are omitted in many demos.
// In production, you should use a real pooling DataSource implementation.
@Override public java.io.PrintWriter getLogWriter() { throw new UnsupportedOperationException(); }
@Override public void setLogWriter(java.io.PrintWriter out) { throw new UnsupportedOperationException(); }
@Override public void setLoginTimeout(int seconds) { throw new UnsupportedOperationException(); }
@Override public int getLoginTimeout() { throw new UnsupportedOperationException(); }
@Override public java.util.logging.Logger getParentLogger() { throw new UnsupportedOperationException(); }
@Override public T unwrap(Class iface) { throw new UnsupportedOperationException(); }
@Override public boolean isWrapperFor(Class iface) { return false; }
}
}
Usage:
import java.sql.Connection;
public class App {
public static void main(String[] args) throws Exception {
try (Connection c = Database.getInstance().getConnection()) {
System.out.println("Got a connection: " + (c != null));
}
}
}
What this buys you:
- A single place to configure database access
- A consistent lifecycle for the expensive part (the
DataSource/ pool)
What it does not buy you:
- “One connection forever” (that’s usually a bug)
If you’re building something larger than a toy program, I also recommend you think about:
- connection pool sizing (too small bottlenecks; too large can overload the DB)
- leak detection (when connections aren’t returned)
- timeouts (connect timeout, socket timeout, statement timeout)
- graceful shutdown (closing the pool)
Those concerns don’t invalidate Singleton; they just remind you that “singleton” is only about identity, not about responsible resource management.
Advantages I See in Real Systems
When people list benefits of Singleton, they often stop at “global access.” That’s only part of the story. The real benefits (when used carefully) are:
- One-time initialization of expensive state (parsers, registries, immutable config snapshots)
- Coordinated access to a shared resource (rate limiter, in-process cache, metrics registry)
- A single point to enforce invariants (for example, a feature-flag client that must always wrap calls with circuit breakers)
- Reduced wiring in very small programs (CLI tools, one-off utilities)
In my experience, Singleton shines in small-to-medium JVM apps where bringing in a full dependency injection container would add more complexity than value.
A good way to phrase the “sweet spot”:
- If the program is small enough that you can reason about globals, a Singleton can reduce friction.
- If the program is large enough that you can’t easily find all the places that touch a global, Singleton becomes a risk multiplier.
Disadvantages (The Ones That Actually Hurt)
Singleton is easy to overuse. The costs show up later, often in testing and in long-lived services.
1) Hidden Dependencies
If a class calls GlobalThing.getInstance() inside its methods, that dependency is invisible to the constructor and hard to replace.
That leads to:
- tight coupling
- “action at a distance” behavior
- harder refactoring
This is the core reason Singleton gets a bad reputation: not because “one instance” is inherently evil, but because global access makes it too easy to smuggle dependencies into code without admitting they exist.
2) Testing Pain
A Singleton is stateful global data unless you keep it strictly immutable.
Common failure mode:
- Test A sets
AppConfigflags. - Test B runs after and sees those flags.
- The suite becomes order-dependent.
If you must have a Singleton, I strongly prefer one of these approaches:
- Make it immutable after startup.
- Make it hold only thread-safe, stateless services.
- Provide a package-private “reset for tests” hook only if you control the full environment (still risky).
I’ll go deeper on testing strategies later because that’s where Singleton decisions either pay off or haunt you.
3) Lifecycle Management and Resource Cleanup
If your Singleton owns a thread pool, file handles, or network clients, you need a shutdown strategy.
In a container-managed world, lifecycle callbacks exist for a reason. Without them, you get:
- non-daemon threads preventing shutdown
- file descriptor leaks
- flaky tests that hang
A rule I follow: if the singleton acquires resources that must be closed, I want it to implement AutoCloseable (even if I don’t always call close() in tiny programs) and I want a clear place in the application where shutdown happens.
4) Multi-Classloader Surprises
In app servers, plugin systems, IDEs, and some hot-reload setups, “one per JVM” is not guaranteed. Singletons are typically “one per class loader.”
If you deploy the same library twice under different class loaders, you can end up with two singletons. Sometimes that’s fine; sometimes it’s a nightmare.
I treat “one per class loader” as the honest contract of Java singletons. If you truly require “one per JVM across class loaders,” you’ve left normal application design and entered specialized territory (and the solution likely isn’t a basic Singleton anymore).
Common Mistakes (And How I Avoid Them)
Here are the mistakes I see most often, plus the fixes I use.
Mistake: Broken via Reflection
Reflection can access private constructors unless you defend against it.
If you use a class-based singleton (not enum), a defensive constructor can reduce risk:
public final class SecureSingleton {
private static boolean constructed = false;
private SecureSingleton() {
if (constructed) {
throw new IllegalStateException("Already constructed");
}
constructed = true;
}
private static final class Holder {
private static final SecureSingleton INSTANCE = new SecureSingleton();
}
public static SecureSingleton getInstance() {
return Holder.INSTANCE;
}
}
This isn’t perfect under all reflection tricks, but it blocks many accidental breakages. If you truly care about reflection safety, the enum approach is the most robust.
One more nuance: reflection is often a symptom, not the disease. If you’re in an environment where untrusted code can use reflection against your internals, you have larger sandboxing and trust-boundary concerns than a singleton can solve.
Mistake: Broken via Serialization
If a singleton is Serializable, deserialization can create a new instance unless you implement readResolve().
import java.io.Serial;
import java.io.Serializable;
public final class SerializableSingleton implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private SerializableSingleton() {}
private static final class Holder {
private static final SerializableSingleton INSTANCE = new SerializableSingleton();
}
public static SerializableSingleton getInstance() {
return Holder.INSTANCE;
}
@Serial
private Object readResolve() {
// Always return the one true instance
return getInstance();
}
}
I prefer enum singletons when serialization is likely because it’s too easy to forget readResolve() during a refactor, and the failure mode can be very confusing (“why do we have two caches now?”).
Also note: Java serialization itself is increasingly avoided in modern systems (for security and maintenance reasons), but it still appears in legacy code, RMI-era integrations, or some frameworks. If serialization is in play, treat it as a first-class design constraint.
Mistake: Broken via Cloning
If your singleton implements Cloneable (or inherits a clone() that can be accessed), you can accidentally duplicate it.
My fix is simple: don’t support cloning for singletons. If there’s any chance clone() exists, override it:
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
More broadly, I try to keep singleton classes final and keep their surface area small. The more features you bolt onto the singleton type (cloning, serialization, reflection-based instantiation), the more holes you create.
Mistake: Lazy Initialization That Does I/O (And Randomly Fails at Runtime)
Lazy init is appealing—until the constructor reads a file, opens a socket, or contacts a remote system. Then the first random request thread that touches getInstance() can trigger I/O.
That creates ugly behavior:
- cold-start latency spikes
- failures that occur “later,” not at startup
- partial initialization issues (some components already running when initialization fails)
If initialization can fail or is expensive, I usually choose one of these:
- Eager initialization during startup (fail fast; simpler operationally)
- Explicit
init()phase that the app calls once, early, and can handle errors - Holder/enum singleton but with only cheap setup in the constructor and explicit “connect” or “warm up” methods controlled by the app
A singleton doesn’t mean “initialization should be implicit.” When failures matter, explicit beats clever.
A Practical, Production-Friendly Config Singleton (Immutable Snapshot)
One of my favorite singleton shapes is an immutable “snapshot” of configuration. It avoids the biggest testing problem (mutable global state) and makes concurrency easy.
import java.time.Duration;
import java.util.Map;
public final class AppConfig {
private final Duration requestTimeout;
private final boolean featureXEnabled;
private final String environment;
private AppConfig(Duration requestTimeout, boolean featureXEnabled, String environment) {
this.requestTimeout = requestTimeout;
this.featureXEnabled = featureXEnabled;
this.environment = environment;
}
public Duration requestTimeout() {
return requestTimeout;
}
public boolean featureXEnabled() {
return featureXEnabled;
}
public String environment() {
return environment;
}
private static AppConfig loadFromSystemProperties() {
Duration timeout = Duration.ofMillis(
Long.parseLong(System.getProperty("request.timeout.ms", "2000"))
);
boolean featureX = Boolean.parseBoolean(
System.getProperty("feature.x.enabled", "false")
);
String env = System.getProperty("app.env", "local");
return new AppConfig(timeout, featureX, env);
}
private static final class Holder {
private static final AppConfig INSTANCE = loadFromSystemProperties();
}
public static AppConfig getInstance() {
return Holder.INSTANCE;
}
// Optional: expose raw values for debugging
public Map debugView() {
return Map.of(
"request.timeout", requestTimeout.toString(),
"feature.x.enabled", Boolean.toString(featureXEnabled),
"app.env", environment
);
}
}
Why this works well in practice:
- No synchronization required after initialization
- No risk of test pollution if tests don’t mutate global system properties mid-suite
- Easy reasoning: the config is a value object, not a magical mutable registry
If you need dynamic config reloading (feature flags, remote config), I still avoid mutating “the singleton” directly. I prefer the singleton to act as a stable façade over an internal, thread-safe reference (for example, an AtomicReference), with an explicit update mechanism.
Performance Considerations (What Actually Matters)
Singleton performance debates often fixate on the wrong thing. In real systems, the cost is rarely “how many nanoseconds does getInstance() take.” The larger costs are:
- initialization cost (I/O, parsing, establishing connections)
- contention if access is synchronized on a hot path
- cache locality and object graph size if the singleton becomes a “god object” holding everything
Still, it’s useful to know the rough performance characteristics:
- Enum / holder: effectively zero overhead after class init (a static field read)
- Eager: same as above, but paid during class loading
- Synchronized accessor: includes lock acquisition even after initialization (JIT can optimize some cases, but I don’t assume it)
- Double-checked locking: fast after init (one volatile read), but more complex
The optimization I care about most: don’t make getInstance() so cheap and ubiquitous that it becomes the default dependency mechanism. It’s not a performance problem—it’s a design entropy problem.
Singleton vs Dependency Injection (My Rule of Thumb)
Singleton often competes with dependency injection (DI). I don’t treat them as enemies; I treat them as tools with different trade-offs.
Here’s my honest rule of thumb:
- If I’m writing a small program, I’m okay with a few carefully chosen singletons.
- If I’m writing a service with many modules, I prefer DI for most dependencies and reserve singleton only for truly global, stable facilities.
Why DI Wins in Larger Codebases
DI gives you something Singleton doesn’t: explicit wiring.
Instead of hiding dependencies behind Foo.getInstance(), you pass them in:
- constructors show what the class needs
- tests can provide fakes without hacks
- lifecycles can be managed (create/close)
Singleton can still exist inside DI: many DI containers create a “singleton-scoped” bean (one instance per container), which is often what people actually want.
A Hybrid Approach I Like
If you’re not ready for a full DI framework, you can still make dependencies explicit with a simple “composition root” (a place where objects are created and wired).
- Your application creates a single
AppContextobject. AppContextconstructs shared services once.- Other code receives those services via constructors.
This gives you “singleton behavior” without global access. The instance is single because you made it single, not because it’s a global static.
I reach for this pattern when I want the discipline of DI without introducing a container.
Multi-Threading and the Java Memory Model (What Breaks and Why)
Singleton is where many developers accidentally learn the Java Memory Model the hard way.
There are two big questions:
1) Can two threads create two instances?
2) Can one thread observe a partially constructed instance?
The minimal lazy singleton fails (1). Incorrect double-checked locking can fail (2).
Why “partially constructed” is even possible: in the presence of reordering and lack of safe publication, another thread can see a reference to an object before the constructor’s writes are fully visible.
Patterns that establish safe publication without you thinking about it:
- enum singletons
- static final field initialization
- initialization-on-demand holder
- correct DCL with a
volatilereference
If you ever feel tempted to invent your own singleton synchronization strategy, treat that as a smell. Prefer the known-good patterns.
Resource Lifecycle: Singletons That Need close()
A singleton that opens resources but never closes them is a common source of:
- hanging JVM shutdown
- flaky tests
- “works locally, leaks in CI” issues
If the singleton owns a resource, I like this shape:
- the singleton implements
AutoCloseable - creation is still singleton-safe (enum/holder)
- application shutdown calls
close()explicitly
Example: a singleton wrapping an HTTP client and an executor.
import java.io.IOException;
import java.net.http.HttpClient;
import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public final class Http {
private final ExecutorService executor;
private final HttpClient client;
private Http() {
this.executor = Executors.newFixedThreadPool(8);
this.client = HttpClient.newBuilder()
.executor(executor)
.connectTimeout(Duration.ofSeconds(2))
.build();
}
private static final class Holder {
private static final Http INSTANCE = new Http();
}
public static Http getInstance() {
return Holder.INSTANCE;
}
public HttpClient client() {
return client;
}
public void close() throws IOException {
executor.shutdown();
}
}
Then, in your application entry point, you make shutdown explicit:
public class Main {
public static void main(String[] args) throws Exception {
try {
// Use Http.getInstance().client() ...
} finally {
Http.getInstance().close();
}
}
}
If you’re tempted to add a shutdown hook:
- I consider it acceptable for small tools.
- For long-lived services, I prefer the container (or your main) to drive shutdown.
Shutdown hooks can hide ordering issues and make tests more confusing because the JVM may keep running for hooks. They’re not “wrong,” just easy to overuse.
Singleton in Containers, Tests, and Parallel Runners
Modern Java projects often run tests in parallel, and some frameworks spin up multiple application contexts.
This is where Singleton surprises people:
- a test that modifies singleton state can affect other tests
- tests running in parallel can race on first initialization
- static singletons persist across multiple tests within the same JVM
My Testing Strategies (In Order)
When a singleton is unavoidable, I choose one of these strategies:
1) Make it immutable after construction.
2) Push mutability behind an interface so tests can swap implementations.
3) Keep state in instance fields but expose no mutators (build new objects instead).
4) Use a controlled reset hook only in test scope (and only if you fully control ordering).
If your singleton is a config snapshot, tests can override configuration by setting system properties before the class is initialized. But that only works if no earlier test triggered class initialization. That’s why I like explicit initialization in test setups when configuration varies.
A Test-Friendly Indirection Pattern
If a bunch of code calls ClockSingleton.now() or FeatureFlags.isEnabled("x"), tests get painful. One pattern I use is to separate:
- a singleton “provider” that returns an interface
- an implementation that can be replaced (carefully) in tests
Example shape:
public interface FeatureFlags {
boolean isEnabled(String key);
}
public final class FeatureFlagsProvider {
private static volatile FeatureFlags instance = new DefaultFeatureFlags();
private FeatureFlagsProvider() {}
public static FeatureFlags get() {
return instance;
}
// Keep this package-private and located in a testable module
static void setForTests(FeatureFlags flags) {
instance = flags;
}
private static final class DefaultFeatureFlags implements FeatureFlags {
@Override
public boolean isEnabled(String key) {
return false;
}
}
}
This is not “pure Singleton” anymore—it’s a global service locator. But it’s often the smallest practical compromise in legacy codebases where refactoring everything to DI is not feasible.
If you can avoid it, do. If you can’t, at least make the override path explicit and contained.
Singleton and Multiple Class Loaders (The Subtle Reality)
I want to restate this because it causes real production weirdness:
- In Java, a “singleton” is typically one per class loader, not one per JVM.
You’ll encounter multiple class loaders in:
- application servers
- plugin architectures
- IDEs
- test frameworks that isolate environments
- hot-reload/dev tools
Symptoms look like:
- two “global” registries that don’t share state
- duplicated background threads
- repeated initialization logs that “should only happen once”
In these environments, I prefer one of these:
- let each module/class loader have its own singleton (often correct)
- use explicit service registration at a higher level (the host app owns the lifecycle)
- avoid static singletons in libraries; let the app inject dependencies
If you’re writing a library, this is one of my strongest recommendations: avoid forcing global state via Singleton. Libraries get loaded in more contexts than you expect.
A Quick Comparison Table (What I’d Choose)
Here’s the cheat sheet I wish more teams used.
Lazy
Serialization-safe
Complexity
—:
—:
—:
if null new) Yes
No
Low
Yes
No (unless readResolve)
Medium
volatile) Yes
No (unless readResolve)
High
Yes
No (unless readResolve)
Low
No
No (unless readResolve)
Low
Usually yes (on first use)
Yes
LowNotes I keep in mind:
- “Reflection-resistant” is never absolute unless you’re relying on stronger platform constraints. Enum is the most robust practical choice.
- Serialization safety is a dealbreaker for some domains; don’t treat it as an afterthought.
When I Use Singleton (Practical Scenarios)
Here are scenarios where Singleton has paid off for me.
1) Logging / Metrics Bridges
Not “a logger for every class” (that’s fine), but a shared bridge like:
- a metrics registry
- a central exporter
- a shared structured logging configuration
I keep it boring and stable: once created, it doesn’t change.
2) Immutable Configuration Snapshot
As shown earlier: a config value object loaded once, used everywhere.
3) In-Process Caches With Clear Invalidation Rules
I’m cautious here because caching is stateful. If I do it, I want:
- thread-safe structures
- a way to observe hit/miss metrics
- a clear eviction policy
4) Expensive, Shared Clients
Examples:
- an HTTP client with connection pooling
- a feature-flag SDK client
- a crypto provider / key store (careful with security boundaries)
Again: if it needs closing, plan for shutdown.
When I Avoid Singleton (Even If It’s Tempting)
These are my “nope” categories.
1) Anything That Represents a User/Request/Session
If the data varies per request, Singleton is the wrong lifetime.
2) Anything With Frequent Runtime Reconfiguration
If the app legitimately needs to swap implementations often, make that explicit via DI or configuration management, not a hidden global.
3) Shared Mutable State Without Strong Concurrency Discipline
If the singleton becomes a dumping ground for mutable maps and caches, you’ll end up debugging race conditions and order-dependent tests.
4) Libraries Intended for Many Environments
Library code loaded by unknown hosts should avoid global static singletons. Let the host decide lifecycles.
A Practical Decision Checklist (I Actually Use This)
When I’m deciding whether to introduce a Singleton, I ask:
1) Do I truly need one instance, or do I just want convenience?
2) Is “one per class loader” acceptable?
3) Can the singleton be immutable after initialization?
4) Does it involve I/O or resources that must be closed?
5) Do tests need to vary its behavior?
6) Would constructor injection make dependencies clearer?
If I answer “yes” to resource ownership or test variability, I lean away from a traditional static singleton and toward explicit wiring or DI.
A Final Word: Singleton Is a Knife, Not a Hammer
Singleton is not inherently bad. The pattern solves a real problem: enforcing a single instance with controlled access.
But the moment you combine “single instance” with “global access,” you create a dependency shortcut that can bypass good design habits.
That’s why my practical stance is:
- Prefer enum or holder for correctness.
- Prefer immutability for sanity.
- Prefer explicit wiring for testability.
- Prefer a clear lifecycle for operational reliability.
Used carefully, Singleton keeps your app consistent and your initialization predictable. Used casually, it becomes the global state you spend months untangling.


