Skip to content

Singleton and Immutable Classes in Java

Singleton Architectural Evolution Imagine your application needs to connect to a database. Every time you run a query, you need a connection. Now imagine if your code created a brand new connection every single time you ran a query. That would be catastrophically expensive. Establishing a database connection involves network negotiation, authentication, resource allocation, and more. You absolutely do not want that happening on every insert or select.

What you want is exactly one connection that gets set up once, stays alive, and every part of your application shares that same connection. That is the core problem the Singleton pattern solves.

A Singleton class has one job: guarantee that no matter how many times you ask for an object of that class, from anywhere in your code, you always get back the same exact object. One object, period. This makes it one of the most frequently asked interview topics in Java because there is real depth here. There are six different ways to implement it, and each one exists because the previous approach had a flaw. You need to understand all six.

What Makes a Class a Singleton

Before looking at the implementations, understand the two structural ingredients every singleton needs.

First, a private constructor. If the constructor is public, anyone can call new DBConnection() and create a fresh object whenever they want. Making it private locks the door. Nobody outside the class can call new on it.

Second, a public static method to hand out the single instance. Since nobody can call new, you need another way to get the object. A static method works because you can call it on the class itself, without needing an object first. DBConnection.getInstance() gives you the one shared object.

With those two rules in mind, let us walk through all six implementations in order. Each one fixes a real problem from the one before it.

The Six Singleton Implementations

Eager Initialization

The simplest approach. You create the object right away, at the moment the class loads into memory.

java
public class DBConnection {

    // Object created immediately when the class is loaded
    // private: nobody outside this class can touch it
    // static: it belongs to the class, not to any instance
    private static DBConnection con = new DBConnection();

    // Private constructor: nobody can do "new DBConnection()" from outside
    private DBConnection() {
        // Set up the actual database connection here
    }

    // Public static method: the only way to get the object
    public static DBConnection getInstance() {
        return con; // always returns the same object
    }
}

This works. It is thread safe because the JVM guarantees that static variables are initialized safely when a class loads. Only one object gets created, and getInstance() always returns it.

The problem is in the name itself. "Eager" means it does the work before you even ask. As soon as the JVM loads this class, that connection gets created. What if the user starts your application but never actually uses the database feature? The connection was created for nothing. Memory and resources are consumed with no benefit.

That is the one disadvantage: no lazy loading. The object exists whether you need it or not.

Lazy Initialization

To fix the eager problem, you delay creating the object until someone actually asks for it.

java
public class DBConnection {

    // Not initialized yet. Just declared. Starts as null.
    private static DBConnection con;

    private DBConnection() {}

    public static DBConnection getInstance() {
        // Only create the object if it does not exist yet
        if (con == null) {
            con = new DBConnection(); // create it on first request
        }
        return con;
    }
}

Now the object is only created when getInstance() is first called. If nobody ever calls it, no connection is ever made. That solves the eager loading waste.

But this introduces a dangerous new problem: threads.

Picture two threads running at the same time, both calling getInstance() for the very first time. Thread 1 checks if (con == null) and sees null. Before it can create the object, Thread 2 also checks if (con == null) and also sees null. Now both threads think they need to create the object. Both execute con = new DBConnection(). Two objects get created. Your singleton is broken.

This race condition is the fatal flaw of lazy initialization.

Synchronized Method

The fix for a race condition is synchronization. Lock the method so only one thread can be inside it at a time.

java
public class DBConnection {

    private static DBConnection con;

    private DBConnection() {}

    // synchronized: only one thread can execute this method at a time
    public static synchronized DBConnection getInstance() {
        if (con == null) {
            con = new DBConnection();
        }
        return con;
    }
}

This is thread safe. When Thread 1 enters getInstance(), it acquires the lock. Thread 2 has to wait outside. Thread 1 creates the object, releases the lock, and Thread 2 enters, finds the object already created, and returns it. Problem solved.

But now you have a performance problem. The synchronized keyword puts a lock on the whole method. Every single call to getInstance() has to acquire and release that lock, even after the object has already been created.

Think about what that means in a real application. Suppose you call getInstance() in a hundred different places, many of them running on different threads. After the very first call, the object exists. Every subsequent call just needs to return a reference that already exists. That is trivially fast, right? Except with synchronized on the method, every one of those hundred calls still has to go through the lock and unlock cycle. Locking is expensive. Your code serializes unnecessarily. This makes it very slow.

You only needed the lock once, during creation. After that, the lock is just overhead.

Double Checked Locking

The insight: only lock during the first creation. After that, let everyone through freely.

java
public class DBConnection {

    // volatile is mandatory here. We will explain exactly why shortly.
    private static volatile DBConnection con;

    private DBConnection() {}

    public static DBConnection getInstance() {
        // Check 1: fast check without locking
        // After creation, this check passes immediately for everyone
        if (con == null) {

            // Only lock when we might need to create the object
            synchronized (DBConnection.class) {

                // Check 2: verify again inside the lock
                // Another thread might have created it between Check 1 and here
                if (con == null) {
                    con = new DBConnection();
                }
            }
        }
        return con;
    }
}

The logic goes like this. The first if (con == null) check runs without any locking. Once the object is created, this check immediately returns false for all future calls, and they skip the synchronized block entirely. Fast.

The only time a thread enters the synchronized block is when con looks null, meaning the object might not exist yet. Inside the lock, it checks again because another thread might have just created the object between the outer check and acquiring the lock. If it is still null, create it. If not, skip it.

This is why it is called "double checked" locking. You check twice. Once fast without a lock, and once safely inside the lock.

Now, about that volatile keyword. It is not optional. It is absolutely required, and here is why.

Why volatile Is Non Negotiable in Double Checked Locking

When you write con = new DBConnection(), it looks like one simple operation. But at the level of the CPU and JVM, it is actually three separate steps:

Step 1: Allocate a chunk of memory on the heap for the new object.

Step 2: Run the constructor to initialize the object's fields.

Step 3: Assign the memory address to the variable con.

Here is the problem: the JVM and CPU are allowed to reorder instructions for performance. In particular, they can swap Step 2 and Step 3. So the actual execution order can become: allocate memory, assign the reference to con, then run the constructor.

After Step 3 (reordered before Step 2), con is no longer null. It points to allocated but uninitialized memory.

Now a second thread comes in and does Check 1: if (con == null). It sees con is not null. It skips the synchronized block entirely and returns con. But con is pointing to an object whose constructor has not finished running yet. Its fields are in an undefined, half initialized state. That thread will try to use a broken object. This causes crashes, corrupted data, or unpredictable behavior.

The volatile keyword prevents this reordering. When a variable is declared volatile, the JVM guarantees that all writes to it are immediately flushed to main memory and that all reads always come from main memory, never from a CPU cache. It also establishes a happens before relationship: everything that happened before writing to con (including the constructor completing) is guaranteed to be visible to any thread that subsequently reads con.

There is actually a second memory issue that volatile fixes. Modern CPUs have multiple cores, each with their own L1 cache. Thread 1 might create the object and store the new reference to con in its core's cache, but that value might not be synced to main memory yet. Thread 2 runs on a different core, checks its own cache or main memory, and sees null because the update has not propagated. Thread 2 then creates a second object.

The volatile keyword solves this too. Writes to a volatile variable always go directly to main memory. Reads always come from main memory. No caching that can hide updates from other threads.

Without volatile, double checked locking is broken in subtle and dangerous ways. With volatile, it works correctly.

The remaining downside of double checked locking is that volatile prevents caching optimizations and synchronized still introduces some overhead. It works well, but there is an even cleaner solution.

Bill Pugh Solution (Static Inner Class)

This is arguably the most elegant approach. It achieves lazy loading, thread safety, and zero locking with no volatile keyword.

java
public class DBConnection {

    // Private constructor prevents outside instantiation
    private DBConnection() {}

    // This nested class is NOT loaded when DBConnection is loaded.
    // It only gets loaded when someone references it for the first time.
    private static class DBConnectionHelper {
        // The object is created here, with the same guarantees as eager init
        private static final DBConnection INSTANCE = new DBConnection();
    }

    public static DBConnection getInstance() {
        // The first time this line runs, DBConnectionHelper gets loaded,
        // which triggers the creation of INSTANCE.
        // The JVM guarantees this is thread-safe.
        return DBConnectionHelper.INSTANCE;
    }
}

Here is why this works so beautifully. When the JVM loads the DBConnection class, it does not automatically load nested classes. DBConnectionHelper sits there completely unloaded. No object is created. So you get lazy initialization for free.

The first time someone calls getInstance(), the JVM references DBConnectionHelper.INSTANCE. That reference causes the JVM to load the DBConnectionHelper class. When a class loads, the JVM executes its static initializers in a thread safe, atomic way. This is guaranteed by the Java Language Specification. So the INSTANCE = new DBConnection() line executes once, safely, no matter how many threads call getInstance() simultaneously.

After that first load, DBConnectionHelper.INSTANCE simply returns the already created object. No locks, no volatile, no synchronization needed on subsequent calls. It is fast and clean.

This solution is also called the "static holder" pattern and is widely recommended in professional Java codebases.

Enum Singleton

The final approach, and arguably the best. Joshua Bloch, author of Effective Java, considers this the gold standard for implementing singletons in Java.

java
public enum DBConnection {
    INSTANCE; // This is the one and only instance

    // Constructor is private by default in enums
    // You cannot write "new DBConnection()" from outside
    
    public void executeQuery(String sql) {
        // run the query
    }
}

// Usage anywhere in your code:
DBConnection.INSTANCE.executeQuery("SELECT * FROM users");

Why is enum the best? Several reasons packed into very few lines.

The JVM guarantees that for any enum, exactly one instance of each enum constant exists per JVM. This is built into the language specification. You get singleton behavior without writing any of the singleton plumbing yourself.

Enum constructors are always private by default. You do not even have to write private. Nobody can call new DBConnection() from outside.

Enums are thread safe by the same JVM class loading guarantees that make eager initialization safe.

And critically, enums are the only singleton implementation that is naturally safe against reflection attacks, which we will cover next.

In only two lines of code, enum gives you everything that the other five implementations require dozens of lines to achieve.

Breaking Singletons: The Classic Interview Question

A common interview question is: "Can you break a singleton?" The answer is yes, you can break most of the classic implementations using reflection.

Here is how reflection breaks a singleton:

java
// Assume DBConnection uses the Bill Pugh pattern
DBConnection instance1 = DBConnection.getInstance();

// Use reflection to access the private constructor
Constructor<DBConnection> constructor = DBConnection.class.getDeclaredConstructor();
constructor.setAccessible(true); // bypass the private restriction!
DBConnection instance2 = constructor.newInstance(); // creates a second object!

// Now instance1 and instance2 are different objects. Singleton is broken.
System.out.println(instance1 == instance2); // prints false

setAccessible(true) overrides the private restriction and lets you call the constructor directly. Two objects now exist.

The defense, for nonenum singletons, is to throw an exception inside the constructor if an instance already exists:

java
private DBConnection() {
    if (DBConnectionHelper.INSTANCE != null) {
        throw new IllegalStateException("Singleton already created. Use getInstance().");
    }
}

But for enum singletons, you do not need this defense at all. The JVM itself prevents reflection from creating additional enum instances. If you try to call newInstance() on an enum constructor via reflection, the JVM throws an IllegalArgumentException automatically. This is one of the biggest reasons enum singleton is considered the gold standard.

Immutable Classes

Now let us shift to immutable classes, which is a closely related and equally important concept.

An immutable class is a class whose objects cannot have their state changed after construction. You create the object, set its values once, and those values stay fixed forever. The most famous example in Java is String. When you do String s = "hello", that string is immutable. You can create a new string from it, but you cannot change the characters inside it.

Why does immutability matter? Immutable objects are inherently thread safe. If nobody can change the state, there is no way for one thread to corrupt data that another thread is reading. You can share immutable objects freely across threads with zero synchronization. They are also easier to reason about because you always know exactly what they contain.

The Five Rules for Creating an Immutable Class

To make a class properly immutable, you follow five rules. Each rule closes a loophole that would otherwise allow someone to change the state.

Rule 1: Declare the class as final.

java
public final class UserProfile {
    // ...
}

If someone can subclass your class, they can override methods and introduce mutable behavior. Making the class final prevents subclassing entirely. No subclass can undermine your immutability guarantee.

Rule 2: Make all fields private and final.

java
public final class UserProfile {
    private final String name;
    private final List<String> petNames;
}

private means nobody outside the class can directly access or change the fields. final means once a value is assigned to the field (in the constructor), it cannot be reassigned. The field will always point to the same value or the same object.

Rule 3: No setter methods.

java
// Do NOT write this:
public void setName(String name) {
    this.name = name; // This would allow mutation. Never do this in an immutable class.
}

Setters exist to change state. An immutable class has no state to change, so it has no setters. Full stop.

Rule 4: Initialize all fields in the constructor.

java
public UserProfile(String name, List<String> petNames) {
    this.name = name;
    this.petNames = new ArrayList<>(petNames); // defensive copy, explained in Rule 5
}

All values come in through the constructor at creation time. After the constructor finishes, no field can ever be changed.

Rule 5: Return defensive copies in getter methods for mutable fields.

This is the most subtle and most commonly misunderstood rule. It deserves a full explanation.

Why Final on a List Does Not Make Its Contents Immutable

This trips up a lot of developers. Look at this field:

java
private final List<String> petNames;

The word final here means the petNames variable will always point to the same list object. You cannot reassign petNames to point at a different list. But it says absolutely nothing about the contents of that list. You can still add to it, remove from it, or change elements inside it.

Think of it like a leash attached to a dog. The leash is fixed (final). The leash always connects to this one specific dog. But the dog can still move around, bark, dig holes. The leash being fixed does not constrain what the dog can do.

Here is a concrete example of how this breaks immutability:

java
public final class UserProfile {
    private final String name;
    private final List<String> petNames;

    public UserProfile(String name, List<String> petNames) {
        this.name = name;
        this.petNames = petNames; // MISTAKE: storing the reference directly
    }

    public List<String> getPetNames() {
        return petNames; // MISTAKE: returning the actual list
    }
}

Now watch what happens:

java
List<String> myPets = new ArrayList<>();
myPets.add("Fluffy");
myPets.add("Max");

UserProfile profile = new UserProfile("Alice", myPets);

// The caller still has the original list
myPets.add("Buddy"); // This changes the list INSIDE the UserProfile object!

// Or even worse:
profile.getPetNames().add("Sneaky"); // This also mutates the internal list!

The object is supposed to be immutable, but its internal state just changed. The final keyword on the field did not prevent this because final only locks the reference, not the referenced object's contents.

The Defensive Copy Technique

The fix is to copy the data at both entry and exit points.

In the constructor, when you receive a mutable object (like a List or a Date), do not store the reference directly. Make a new copy and store that instead. Now changes to the original list outside the class cannot affect your internal copy.

In the getter, do not return the actual internal field. Return a new copy. That way, even if the caller modifies what they get back, your internal data is untouched.

java
public final class UserProfile {

    private final String name;
    private final List<String> petNames;

    public UserProfile(String name, List<String> petNames) {
        this.name = name;
        // Defensive copy in constructor:
        // Create a new list so the caller's list and our list are separate
        this.petNames = new ArrayList<>(petNames);
    }

    public String getName() {
        // String is already immutable, safe to return directly
        return name;
    }

    public List<String> getPetNames() {
        // Defensive copy in getter:
        // Return a new list so callers cannot modify our internal list
        return new ArrayList<>(petNames);
    }
}

Now see how the behavior changes:

java
List<String> myPets = new ArrayList<>();
myPets.add("Fluffy");
myPets.add("Max");

UserProfile profile = new UserProfile("Alice", myPets);

// Modifying the original list has no effect on the profile
myPets.add("Buddy");
System.out.println(profile.getPetNames()); // prints [Fluffy, Max]

// Modifying what getPetNames() returns has no effect either
List<String> returned = profile.getPetNames();
returned.add("Sneaky");
System.out.println(profile.getPetNames()); // still prints [Fluffy, Max]

Each call to getPetNames() creates a fresh list that is a copy of the internal data. If someone adds to the copy, they are adding to their own copy, not yours. Your original data stays protected.

The caller who adds "Sneaky" to the returned list is holding a reference that nobody else has. When that reference goes out of scope, the garbage collector cleans it up. Your object's internal petNames never changes.

A Complete Immutable Class Example

Putting all five rules together:

java
// Rule 1: class is final, no subclassing allowed
public final class UserProfile {

    // Rule 2: all fields are private and final
    private final String name;
    private final int age;
    private final List<String> petNames;

    // Rule 4: all values set once in the constructor
    public UserProfile(String name, int age, List<String> petNames) {
        this.name = name;
        this.age = age;
        // Rule 5 (constructor side): defensive copy of the mutable list
        this.petNames = new ArrayList<>(petNames);
    }

    // Rule 3: only getters, no setters
    public String getName() {
        return name; // String is immutable, safe to return directly
    }

    public int getAge() {
        return age; // primitive, copied by value, safe to return directly
    }

    // Rule 5 (getter side): return a copy, not the actual internal list
    public List<String> getPetNames() {
        return new ArrayList<>(petNames);
    }
}

This object, once created, can never have its internal state changed. It is safe to share across threads. It is predictable. It is honest about what it is.

Interview Questions and Pitfalls to Know

Question: Why does the synchronized method singleton perform badly?

Because the lock is acquired and released on every single call to getInstance(), even after the object has already been created. Most calls are just reading a reference that already exists. There is no reason to lock for a read of an already stable reference. The lock is overhead with no benefit after the first call.

Question: What is the difference between the two null checks in double checked locking?

The first check is without a lock. It is the fast path. For every call after the object is created, this check returns false and the method returns immediately, no locking needed.

The second check is inside the synchronized block. It exists because two threads could pass the first check simultaneously (both see null before either creates the object). Once one thread acquires the lock and creates the object, the other thread enters the lock and needs to verify again that the object was not just created by the first thread.

Question: Why is volatile required in double checked locking?

Two reasons. First, without volatile, a CPU core might cache the write to con in its L1 cache without flushing it to main memory. Other cores checking main memory would still see null and create duplicate objects.

Second, without volatile, the JVM or CPU can reorder the three steps of object creation (allocate, initialize, assign reference). It can assign the reference before the constructor finishes. Another thread sees a nonnull reference and skips the synchronized block, but then uses an incompletely initialized object. Volatile prevents both caching issues and instruction reordering.

Question: Why is enum the best singleton implementation?

Because it provides lazy loaded (at class load time), thread safe singleton behavior in two lines of code. It is naturally protected against reflection attacks by the JVM itself (attempting to create an enum instance via reflection throws an exception). It is also safe against deserialization creating duplicate instances. You get all the guarantees of a proper singleton with none of the boilerplate.

Question: What is wrong with making all fields final to create an immutable class?

Final on a reference type field only prevents reassigning the reference. It does not prevent mutating the object the reference points to. A final List still allows add(), remove(), and set(). You need defensive copies in both the constructor and getters to truly protect mutable fields.

Question: Can you break a singleton? How would you defend against it?

Yes. Using reflection, you can call setAccessible(true) on the private constructor and invoke it to create a second instance. The defense is to throw an IllegalStateException inside the constructor if an instance already exists. Better yet, use an enum singleton, which the JVM itself protects from reflection based attacks.

Question: What is the difference between eager and lazy initialization in singletons?

Eager initialization creates the object when the class loads, regardless of whether anyone ever uses it. Lazy initialization waits until the first call to getInstance(). Eager is simpler and thread safe but wastes resources if the singleton is never used. Lazy avoids that waste but requires careful synchronization to be thread safe.

Question: Why does the Bill Pugh solution not need volatile or synchronized?

Because it relies on the JVM's class loading guarantees. The static initializer inside the nested class (DBConnectionHelper) runs exactly once, atomically, the first time the class is referenced. The JVM guarantees this is thread safe at the class loading level, no extra synchronization needed. And since the nested class is not loaded until first reference, you also get lazy initialization for free.

Question: In an immutable class, do you need defensive copies for String fields?

No. String is itself immutable in Java. Even if you return a String reference directly, the caller cannot change the string it points to. Defensive copies are only needed for mutable types like List, Set, Map, Date, arrays, and custom mutable classes.

Summary

The Singleton pattern enforces one instance per JVM by combining a private constructor with a public static accessor. You need to understand all six implementations because each one addresses a real world tradeoff between simplicity, lazy loading, thread safety, and performance.

Eager initialization is simple and safe but wasteful. Lazy initialization is efficient but thread unsafe. Synchronized method is safe but slow. Double checked locking is fast and safe but requires volatile without which subtle bugs around CPU caching and instruction reordering will break it. Bill Pugh uses the JVM's own class loading guarantees to get lazy, thread safe initialization without any synchronization primitives. Enum singleton is the gold standard: concise, safe against reflection, and backed by JVM guarantees.

Immutable classes make objects whose state never changes after construction. The five rules are: declare the class final, make all fields private and final, provide no setters, initialize everything in the constructor, and return defensive copies from getters for mutable fields. The most critical nuance is understanding that final on a collection reference does not protect the collection's contents. Only defensive copying does.

These two patterns, Singleton and immutable, are fundamental building blocks of thread safe, predictable Java programs. Master both and you will handle a large portion of Java design pattern interview questions with confidence.