Low-level designCore8 min

Singleton, and why injection usually beats it

A Singleton is a class that allows exactly one instance and hands it out from a static method. It solves "there must be only one", and adds a global that every caller depends on. Creating one instance at the root and passing it in usually gives the first without the second.

Watch it evolve1 / 9
The problem

Two ways to join one queue

The kiosk and the phone line both hand out queue numbers, each from its own counter.

80
Start
Stage 1
Stage 2
Final

1 · The situation

Two desks, two counters, one queue

A walk-in clinic hands out queue numbers. Patients can take one from a kiosk at the door or by calling the phone line, and they're all called in one order. TicketCounter.next() returns the next number. The first version gives the kiosk and the phone line a counter each.

The program also runs a small test: at the start of a new day, numbering should begin again at 1.

The first versionJava
class TicketCounter {
    private int last;

    int next() {
        return ++last;
    }
}

class Kiosk {
    private final TicketCounter counter = new TicketCounter();

    String issue() {
        return "Kiosk: ticket " + counter.next();
    }
}

class PhoneLine {
    private final TicketCounter counter = new TicketCounter();

    String issue() {
        return "Phone: ticket " + counter.next();
    }
}

public class Main {
    public static void main(String[] args) {
        Kiosk kiosk = new Kiosk();
        PhoneLine phone = new PhoneLine();
        System.out.println(kiosk.issue());
        System.out.println(phone.issue());
        System.out.println(kiosk.issue());

        // A test: each new day's numbering starts at 1.
        TicketCounter fresh = new TicketCounter();
        int first = fresh.next();
        System.out.println(first == 1 ? "Test passed: a new day starts at ticket 1"
                : "Test failed: expected ticket 1, got " + first);
    }
}
11TicketCounter− last: int~ next(): intKiosk− counter: TicketCounter~ issue(): StringPhoneLine− counter: TicketCounter~ issue(): String

The class diagram of this version, drawn from its code: each desk owns its own counter.

Kiosk: ticket 1
Phone: ticket 1
Kiosk: ticket 2
Test passed: a new day starts at ticket 1

Two patients hold ticket 1.

2 · The change

There must be exactly one counter

Two counters means two sequences, and two patients with the same number. The fix everyone reaches for first is to make it impossible to create a second counter: a Singleton. The constructor becomes private, the class holds its one instance, and a static method hands it out:

One instance, available everywhereJava
final class TicketCounter {
    private static final TicketCounter INSTANCE = new TicketCounter();
    private int last;

    private TicketCounter() {}

    static TicketCounter instance() {
        return INSTANCE;
    }

    int next() {
        return ++last;
    }
}
Kiosk: ticket 1
Phone: ticket 2
Kiosk: ticket 3
Test failed: expected ticket 1, got 4

The queue numbers are right. The test isn't.

The duplicates are gone, and the checks recognize a Singleton. But the test fails: it asks for the counter and gets the same one the desks used, already at 3. There's no way to get a fresh counter, because "a fresh counter" is exactly what the class forbids.

What the Singleton gave

What it cost

Exactly one counter in the program

Exactly one counter in every test, carrying state from test to test

Any class can reach it, with no wiring

No class says it needs it; the dependency is hidden inside methods

No one can create a second by mistake

No one can create a second on purpose, such as a counter per clinic

3 · The fix

One instance by choice, not by force

Start from the first version. The real requirement is "the desks share one counter", not "the program can never make two". So make one, and share it:

Kiosk and PhoneLine take a TicketCounter in their constructors instead of creating one or looking one up.

Java
class Kiosk {
    private final TicketCounter counter;

    Kiosk(TicketCounter counter) {
        this.counter = counter;
    }

    String issue() {
        return "Kiosk: ticket " + counter.next();
    }
}

class PhoneLine {
    private final TicketCounter counter;

    PhoneLine(TicketCounter counter) {
        this.counter = counter;
    }

    String issue() {
        return "Phone: ticket " + counter.next();
    }
}
TicketCounter− last: int~ next(): intKiosk− counter: TicketCounter~ issue(): StringPhoneLine− counter: TicketCounter~ issue(): String

main creates one counter and passes the same one to both desks:

One counter, created once and sharedJava
class TicketCounter {
    private int last;

    int next() {
        return ++last;
    }
}

class Kiosk {
    private final TicketCounter counter;

    Kiosk(TicketCounter counter) {
        this.counter = counter;
    }

    String issue() {
        return "Kiosk: ticket " + counter.next();
    }
}

class PhoneLine {
    private final TicketCounter counter;

    PhoneLine(TicketCounter counter) {
        this.counter = counter;
    }

    String issue() {
        return "Phone: ticket " + counter.next();
    }
}

public class Main {
    public static void main(String[] args) {
        TicketCounter counter = new TicketCounter();
        Kiosk kiosk = new Kiosk(counter);
        PhoneLine phone = new PhoneLine(counter);
        System.out.println(kiosk.issue());
        System.out.println(phone.issue());
        System.out.println(kiosk.issue());

        // A test: each new day's numbering starts at 1.
        int first = new TicketCounter().next();
        System.out.println(first == 1 ? "Test passed: a new day starts at ticket 1"
                : "Test failed: expected ticket 1, got " + first);
    }
}
Kiosk: ticket 1
Phone: ticket 2
Kiosk: ticket 3
Test passed: a new day starts at ticket 1

Unique numbers for patients, and a fresh counter for the test.

4 · The pattern

Singleton: one instance, one global access point

Singleton ensures a class has exactly one instance and gives global access to it. It combines two things: a guarantee (no second instance can exist) and a global (any code can reach it by calling a static method). The guarantee is occasionally what you need; the global is what usually causes trouble.

TicketCounter− INSTANCE: TicketCounter− last: int~ instance(): TicketCounter~ next(): intKiosk~ issue(): StringPhoneLine~ issue(): String

The Singleton version: TicketCounter holds its own single instance, and each desk reaches for it.

Part

In the clinic's code

Its job

Private constructor

TicketCounter()

Stops anyone else creating one. JavaScript and Python have no private constructors, so there it refuses unless instance() calls it: that stops accidents, not a caller set on getting round it

The instance

A static field, or a static local in C++

Holds the one object

Access point

TicketCounter.instance()

Hands the instance to any caller

Both desks reach the same instance through the static method; neither was told it would.

The injected version keeps what mattered, one counter shared by both desks, and drops the global. The counter is an ordinary class that happens to be created once, an approach sometimes called "just create one".

5 · What it costs

A global with a constructor guard

  • Hidden dependencies. A class that calls instance() inside a method depends on the singleton, and nothing in its constructor or type says so.
  • Shared state between tests. One instance per process means state left by one test is seen by the next.
  • No second instance, ever. A second clinic, or a counter per department, needs the class rewritten.
  • It does two jobs. The class counts tickets and also polices how many of itself exist.

A real Singleton is defensible when a second instance would be a bug everywhere: an object that wraps a resource the process can only open once, such as a hardware device or the process's own logging output. Even then, keep the static access in the composition root and pass the instance on from there.

Need

Better answer

Several classes share one object

Create one at the root and pass it in

Configuration read once at startup

A value object created at startup and passed in

A cache or pool shared by the program

One instance from the root; see Object Pool

A resource that truly can't be opened twice

A Singleton, accessed only from the root

6 · Getting it right

If you must, make it safe and keep it at the edge

  • Make creation thread-safe. Two threads calling a lazy instance() at once must not create two objects.
  • Prefer eager or language-guaranteed creation over hand-written locking.
  • Don't call instance() deep inside classes. Fetch it once at the root and inject it.
  • Give it an interface if tests need to replace it.

Language

The usual safe form

Why it's safe

Java

A static final field, or an enum with one constant

Class initialization runs once; enum constants are the only instances

C++

A static local variable in instance()

Since C++11 its initialization happens exactly once, even with several threads

JavaScript

Export one instance from a module

A module is evaluated once, and every importer shares it

Python

A module-level instance

A module runs once; later imports reuse it

In JavaScript and Python, a module-level instance is the idiomatic singleton, and it has the same trade-off: easy to reach from anywhere, hard to replace in a test.

7 · Look-alikes

One instance, many ways

Several designs end up with one shared object. They differ in who controls it and how others find it.

Design

Who creates the one instance

How others get it

Singleton

The class itself

A static access method

Created once and injected

The composition root

Constructor parameters

Service locator

Whoever registers it

A lookup in a global registry

Static class

No instance at all

Static methods; no state to replace

Object Pool

The pool, several instances

Borrow and return

The checks recognize the Singleton version as a Singleton, and Design anti-patterns explains why a recognized shape isn't an endorsement. The injected version has one counter too; it just doesn't force it.

8 · In practice

Where singletons hide

Frameworks often manage "singleton" objects for you: a dependency injection container creates one instance of a service and injects it everywhere. That's the injected version, done by a container, not the pattern with a static access method.

The practice checks recognize a Singleton when a class has a private constructor and a static method that returns its own instance; they flag a public constructor next to one as critical, and a second instance created elsewhere as a warning. Design the parking lot, and notice whether the garage needs to be one-of-a-kind, or just created once.

Check yourself

4 questions

1. The clinic opens a second site with its own queue. What's needed in each version?
2. Why did the test fail in the Singleton version?
3. Which is the strongest case for a true Singleton?
4. How is a C++ singleton made thread-safe most simply?

Takeaways

Remember this

  • Singleton combines a guarantee of one instance with a global access point.
  • The global is the costly part: hidden dependencies and state shared between tests.
  • Usually what's needed is one shared instance; create it at the root and inject it.
  • Keep a true Singleton for resources where a second instance would be a bug everywhere.
  • If you write one, use the language's guaranteed-once initialization and access it only from the root.