Singleton

Part of the free Low-Level Design course on LogicWiz, in the chapter: Creational patterns. The class-diagram exercises run in the app.

Two customers, one order number

"Two customers got order SK-1042 this morning. Arjun, how many ID generators does this app have?"

Priya, in the incident channel


The problem: one thing that must stay one

Every SwiftKart order gets a number such as SK-1042. Support reads it out on calls, and payments sends it to the provider. Two orders with one number is a real bug. A refund for SK-1042 can reach the wrong customer.

Here is Arjun's ID generator, and the two services that use it.

public class OrderIdGenerator {
    private final AtomicLong next = new AtomicLong(1000);

    public String nextId() {
        return "SK-" + next.incrementAndGet();
    }
}

public class OrderService {
    private final OrderIdGenerator ids = new OrderIdGenerator();
    // place(cart) calls ids.nextId()
}

public class ReorderService {
    private final OrderIdGenerator ids = new OrderIdGenerator();
    // reorder(oldOrder) calls ids.nextId()
}

AtomicLong is a number from java.util.concurrent that many threads can increase safely. It makes one generator safe. It cannot help when there are two. Each service created its own generator, and each one starts at 1000. Both hand out SK-1001, SK-1002 and so on, and the numbers collide.

App settings have the same shape. Loading SwiftKart's config file is slow, and two copies can disagree after one of them reloads.

The real problem: nothing in the code says "there must be only one". Any class can call new OrderIdGenerator().

The idea: one copy, and one way to reach it

Think of the token machine at a bank counter. Everyone takes a numbered token from the same machine, so no two people hold the same number. If the bank installed a second machine that also started at 1, two customers would both hold token 42.

So Arjun makes the class refuse to be created twice. He also gives every caller one way to reach the single copy. That is the singleton pattern: a class that ensures only one instance of itself exists, and gives global access to it.

Notice that a singleton makes two promises:

  • Only one: the class itself prevents a second instance.
  • Reachable from anywhere: any line of code can get the instance through a static method.

Keep them apart in your head. The first is often what you need. The second is where the trouble comes from.

The analogy stops here. You walk up to the bank's machine on purpose, and everyone can see you do it. A singleton can be reached from inside any method, and nothing in the method's signature shows it.

The structure

classDiagram
  class Singleton {
    -Singleton instance$
    -Singleton()
    +getInstance()$ Singleton
    +nextId() String
  }
  Client ..> Singleton : getInstance()

There is one role, the Singleton, with three parts:

  • a private constructor, so no other class can call new,
  • a private static field that holds the one instance,
  • a public static method, usually getInstance(), that returns it.

A static member belongs to the class itself, not to any one object. That is what makes the field shared and the method callable from anywhere.

Build it step by step

First, the classic version.

public class OrderIdGenerator {
    private static OrderIdGenerator instance;
    private final AtomicLong next = new AtomicLong(1000);

    private OrderIdGenerator() { }

    public static OrderIdGenerator getInstance() {
        if (instance == null) {
            instance = new OrderIdGenerator();
        }
        return instance;
    }

    public String nextId() {
        return "SK-" + next.incrementAndGet();
    }
}

Callers write OrderIdGenerator.getInstance().nextId(). The instance is created on the first call. Creating an object only when it is first needed is called lazy initialisation.

This version is not thread-safe. Two threads can call getInstance() at the same moment, before the instance exists. Both see null, and both create a generator. For a moment there are two, and they can hand out the same number. A bug that depends on the timing of threads like this is called a race condition.

Second, two standard fixes. Both use one promise from Java: a class's static fields are set up once, by one thread, the first time the class is used.

// Eager: created when the class loads, once and safely.
public class OrderIdGenerator {
    private static final OrderIdGenerator INSTANCE = new OrderIdGenerator();
    private OrderIdGenerator() { }
    public static OrderIdGenerator getInstance() { return INSTANCE; }
}

// Holder idiom: lazy and safe, with no locks.
public class AppConfig {
    private AppConfig() { /* read the settings file */ }

    private static class Holder {
        static final AppConfig INSTANCE = new AppConfig();
    }

    public static AppConfig getInstance() { return Holder.INSTANCE; }
}

Eager initialisation creates the instance when the class loads, even if nobody uses it. For a small object, that is fine. The holder idiom puts the instance in a nested class. Java loads Holder only when getInstance() first touches it, so the slow config file is read only when needed.

Third, the shortest safe version: an enum with one value.

public enum OrderIds {
    INSTANCE;

    private final AtomicLong next = new AtomicLong(1000);

    public String nextId() {
        return "SK-" + next.incrementAndGet();
    }
}

Callers write OrderIds.INSTANCE.nextId(). Java guarantees exactly one object per enum constant, even through serialisation (saving an object to bytes and reading it back). The downside is that an enum cannot extend another class.

You may also see synchronized on getInstance(). It works, but every call takes a lock. Another version, double-checked locking, is correct only with a volatile field and is easy to get wrong. Prefer the three versions above.

What a singleton costs

Thread safety fixed "only one". It did nothing about "reachable from anywhere", and that brings three costs.

  • Hidden dependencies. new OrderService() looks like it needs nothing. Inside, it calls OrderIdGenerator.getInstance(). The constructor no longer tells the truth about what the class uses.
  • Hidden global state. Any code can read or change the instance, at any time. When a value is wrong, every class is a suspect.
  • Hard to test. A test checks that the first order is SK-1001. But the counter lives for the whole test run, and another test already took 1001. Tests pass alone and fail together. You also cannot pass a fake, because callers name the concrete class.

There is a fourth, slower cost. When SwiftKart adds a second series for refunds, "RF-1001", the class that promised "only one" must be rewritten.

A candidate in a mock interview said, "There is only one parking lot, so ParkingLot must be a singleton." The interviewer asked two questions. How do you reset it between tests? And what happens when the company opens a second lot? "Only one exists" is a fact about how the app is set up today. A singleton turns that fact into a rule inside the class, and adds global access on top.

The better way: create one, pass it in

You want one instance. You do not need the class itself to enforce it. Create the object once, where the app starts, and pass it to every class that needs it.

public interface IdGenerator {
    String nextId();
}

public final class Main {
    public static void main(String[] args) {
        IdGenerator ids = new SequentialIdGenerator("SK-", 1000);
        OrderService orders = new OrderService(ids);
        ReorderService reorders = new ReorderService(ids);
        System.out.println(orders.place(demoCart()).id());
    }
}

SequentialIdGenerator is the old generator with two changes. It implements IdGenerator, and its public constructor takes the prefix and the starting number. The refund series is now one more object, new SequentialIdGenerator("RF-", 1000), and no class is rewritten.

main here is the composition root: the one place where the app creates its objects and connects them. Both services receive the same ids object, so they share one counter and never collide. A test creates its own SequentialIdGenerator, so the first ID is always SK-1001. Each constructor shows what the class needs. This is the constructor injection from the SOLID chapter, used to get "only one".

Can someone still create a second generator by mistake? Yes, in principle. In practice, only main creates it, and a reviewer can check that in one file. That is a small price for code you can test.

A singleton is still acceptable for something truly process-wide whose state tests never check, such as a logger, or a read-only lookup table loaded once. If you write one, the enum form is the simplest safe version. Frameworks such as Spring talk about "singleton beans". That means one instance per application, created by the framework and passed in. It is the composition-root idea, not this pattern.

Do not confuse a singleton with a class of only static methods. Both are global, but the static class cannot implement an interface, so you cannot swap it in a test either. In interviews, the question comes up with a logger, a parking lot, a config or a cache.

💡 Tip: In an interview, say "I will create one instance in main and inject it." Then offer to show a thread-safe singleton. It shows you know the pattern and its costs.

OrderService and ReorderService below both call OrderIdGenerator.getInstance(). Change the design so they receive the generator through their constructors, behind an interface, so a test can pass its own.

📌 Summary: A singleton promises one instance and global access. Make it thread-safe with eager creation, a holder or an enum. Usually better: create one instance in main and pass it in.

What Arjun learns next

Arjun can now create objects cleanly. Next, he looks inside RazorpayCharger and PayUCharger. The two SDKs they call have different shapes, and vendor code cannot be changed. Making them fit one interface is the job of the adapter.

Check your understanding

  1. A test checks that the first order is SK-1001. It passes alone but fails in the full suite. The code calls OrderIdGenerator.getInstance(). What is the likely cause?
  2. Two threads call the lazy getInstance() at the same moment, before any instance exists. What can happen?
  3. An interviewer asks: "Should ParkingLot be a singleton, since there is only one lot?" What is the strongest answer?
  4. You must write a singleton that is created only on first use and needs no lock on each call. Which version fits?