Let’s be honest for a second. Most developers think they know how to write a Singleton. They read a blog post ten years ago and never checked if the information was still relevant to the modern JVM. We are told that Singletons are “evil”—that they are hard to test and create toxic global state.
The industry loves to follow trends. We went from “Singletons everywhere” to “Use a Dependency Injection framework for everything.” Now, I see developers pulling in Spring or Guice just to manage a single instance of a Logger. It adds 50 megabytes to your JAR and makes the application start 10 seconds slower.
Singletons are not the problem. The problem is your implementation. Most “Senior” developers I meet still write Singletons that aren’t thread-safe, or they write code so complex it becomes a maintenance liability. We need to stop the “Resume-Driven Development” where we use complex patterns just to look smart and start focusing on how the JVM actually works.
The Reality of Failure: Common Anti-Patterns
Before we look at the right way, we need to identify the code that is likely rotting in your codebase right now.
1. The Eager Initialization
This is the simplest way. You create the instance when the class is loaded.
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
This works and it is thread-safe because the JVM handles static initialization. However, if this class is heavy, you are slowing down your application start-up. If you never use this instance during a specific run, you wasted memory for nothing. In a modern cloud environment, we want fast start-up times; this approach is often too heavy.
2. The Broken Lazy Initialization
Developers trying to be “efficient” often write this disaster:
public class LazyBrokenSingleton {
private static LazyBrokenSingleton instance;
private LazyBrokenSingleton() {}
public static LazyBrokenSingleton getInstance() {
if (instance == null) {
instance = new LazyBrokenSingleton();
}
return instance;
}
}
This code is a disaster in production. It works on your laptop with a single user, but it fails under load. Two threads call getInstance() simultaneously, both see a null instance, and both create a new object. If this class manages a file or a socket, your application will eventually crash.
3. The Double-Checked Locking
This is what people write when they want to prove they are “Senior.”
public class DoubleCheckedSingleton {
private static volatile DoubleCheckedSingleton instance;
private DoubleCheckedSingleton() {}
public static DoubleCheckedSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckedSingleton.class) {
if (instance == null) {
instance = new DoubleCheckedSingleton();
}
}
}
return instance;
}
}
Look at this boilerplate. You need the volatile keyword to prevent the Java Memory Model from reordering instructions—otherwise, a thread might see a partially initialized object. Why are we writing ten lines of code for something that should be one line? This isn’t “clean code”; it’s a liability.
The Professional Standard: How to Write Singletons in 2026
If you aren’t using a framework to manage your instances, there are only two ways you should be doing this.
4. The Bill Pugh Singleton (The Holder Idiom)
This provides lazy loading without the complexity of manual synchronization.
public class BillPughSingleton {
private BillPughSingleton() {}
private static class SingletonHolder {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
The SingletonHolder class is not loaded into memory until getInstance() is called. The JVM guarantees that class loading is thread-safe. You get lazy loading and thread safety with zero boilerplate.
5. The Enum Singleton: The Gold Standard
This is the recommendation from Effective Java. If you want to be a professional, use an Enum.
public enum EnumSingleton {
INSTANCE;
public void processData() {
// Implementation logic
}
}
The Enum is the only implementation that is truly bulletproof. The JVM provides several guarantees:
- No Reflection Attacks: You cannot use reflection to instantiate a second instance.
- Serialization: It handles deserialization automatically without creating new instances.
- Thread Safety: The instance is safe by default.
The Psychology of Over-Engineering
Why do we still see the bad versions? It’s often the Sunk Cost Fallacy. We spent years learning how volatile and synchronized work, and we feel like we aren’t adding value if the solution is too simple.
True seniority is choosing the simplest tool that works. Complexity is debt. Every line of extra code is something that can break at 3 AM. Stop trying to impress your coworkers and start writing code that allows you to sleep.
Addressing the “Testing” and “Memory” Myths
I know what you’re thinking.
“But I can’t mock an Enum with Mockito!”
If you are trying to mock a Singleton, your architecture is already broken. Your classes should depend on an interface, not a concrete implementation.
public interface Configuration {
String getValue(String key);
}
public enum AppConfig implements Configuration {
INSTANCE;
@Override
public String getValue(String key) {
return "value";
}
}
Now, pass the Configuration interface to your classes. In your tests, pass a mock. In production, pass AppConfig.INSTANCE.
“What about memory leaks?”
A Singleton is meant to live for the duration of the application. If you have a “Singleton” that needs to be destroyed and recreated, it is not a Singleton—it is a session-scoped object. Use the right name for your patterns.
Actionable Strategy for Your Monday Morning
Don’t start a massive refactoring that loses your manager’s trust. Follow this roadmap instead:
- Audit for
getInstance(): Look at every Singleton in your project. - Fix Thread Safety: If you see the “Lazy Broken” pattern, replace it immediately with an Enum or the Bill Pugh holder.
- Remove the Boilerplate: Replace complex double-checked locking with the Holder Idiom to improve readability.
- Decouple with Interfaces: Ensure your Singletons implement interfaces so you can stop passing concrete classes around.
- Monitor State: If your Singleton holds a growing list of objects, verify if it needs a cleanup mechanism or if that state belongs elsewhere.



Leave a comment