RizTech Academy logo
RizTech Academy
Design Patterns in PracticeLesson 3 of 730 min

Singleton, and why dependency injection replaced it

Singleton is the pattern everybody learns first and the one most likely to be a mistake. Both facts are worth understanding, because you will inherit code full of them.

What it is

One instance, globally reachable.

public final class RateCard {

    private static final RateCard INSTANCE = new RateCard();

    private final Map<Plan, Long> prices = new EnumMap<>(Plan.class);

    private RateCard() {
        prices.put(Plan.VEG, 8_000L);
        prices.put(Plan.JAIN, 9_000L);
    }

    public static RateCard getInstance() {
        return INSTANCE;
    }

    public long priceOf(Plan plan) {
        return prices.getOrDefault(plan, 0L);
    }
}

A private constructor so nobody else can make one, a static field holding the only instance, and a static accessor.

In Java, the right way to write one is an enum:

public enum RateCard {
    INSTANCE;

    private final Map<Plan, Long> prices = new EnumMap<>(Plan.class);

    RateCard() {
        prices.put(Plan.VEG, 8_000L);
        prices.put(Plan.JAIN, 9_000L);
    }

    public long priceOf(Plan plan) {
        return prices.getOrDefault(plan, 0L);
    }
}

The JVM guarantees an enum constant is created once, thread-safely, and it is the only form that survives serialisation and reflection without extra work. Every other approach — double-checked locking, holder classes, volatile — exists to solve problems the enum does not have.

So if you must write one, write that. The rest of this lesson is about why you usually should not.

Why it is usually wrong

It hides a dependency. Look at this signature:

public long billFor(String customer, int tiffins)

Nothing tells you it reaches for a RateCard. The class's real dependencies are scattered through its method bodies where nobody reading the constructor will find them.

It makes testing hard. To test billing with a different rate card, you have to change global state — and then change it back, and hope no test runs in parallel. The test suite acquires an order dependence nobody put there on purpose.

// What you want to write:
BillingService billing = new BillingService(testRates);

// What a singleton forces:
RateCard.getInstance().setPrices(testRates);   // and now every other test sees it

It is global mutable state. Everything the last forty years taught about global variables applies. A singleton is a global variable with a constructor.

One instance is a guess about the future. One database. One rate card. One configuration. Then the shop opens a second branch with different prices, and "one" turns out to have been an assumption rather than a fact.

Dependency injection is the answer

The whole of it, in one idea: a class asks for what it needs, and somebody else decides what to give it.

public final class BillingService {

    private final Map<String, Subscriber> subscribers;

    public BillingService(List<Subscriber> subscribers) {
        this.subscribers = subscribers.stream()
                .collect(Collectors.toMap(Subscriber::name, s -> s, (a, b) -> a, LinkedHashMap::new));
    }
}

That is the capstone's actual BillingService, and it is dependency injection — no framework involved. The dependency is in the constructor, so:

  • A reader knows what the class needs without reading a single method.
  • A test passes whatever subscribers it likes, with no global state.
  • Two BillingService instances with different subscribers can exist at once.
  • The compiler stops you constructing one without its dependency.

"Dependency injection" is mostly just constructor parameters. Spring and Guice automate the wiring when there are hundreds of objects; the idea does not need them, and you should be comfortable with it before meeting a framework that hides it.

Where the object actually gets made

The obvious objection: something has to construct it eventually, so have you not just moved the problem?

You have moved it, and that is the point. It moves to one place, usually main:

public static void main(String[] args) {
    DeliveryStore store = new DeliveryStore(Path.of("deliveries.csv"));
    BillingService billing = new BillingService(loadSubscribers());
    new Cli(store, billing).run(args);
}

This is sometimes called the composition root. Everything below it receives what it needs and constructs nothing global. One place in the program knows how the pieces fit together, and it is the first place a new reader should look.

When a singleton is fine

Being fair to it, because "never" is not true:

Genuinely stateless utilities. Math, Collections, Objects — static methods on a final class with a private constructor. No state, nothing to mock, nothing to reset.

Enum constants. Plan.VEG is a singleton and entirely correct.

Things the platform genuinely has one of. A connection pool, a metrics registry. Even then, prefer injecting it and letting one place decide there is only one.

The test is state. A singleton holding no mutable state is a namespace, which is fine. A singleton holding mutable state is a global variable, which is not.

Reading the code you will inherit

You will join a codebase with getInstance() everywhere. What to do:

Do not rewrite it all. That is a large change with no visible benefit and it will not be approved.

Do stop adding more.

When you need to test a class that reaches for a singleton, add a constructor that takes the dependency and have the no-argument constructor pass getInstance():

public BillingService() {
    this(RateCard.INSTANCE);
}

public BillingService(RateCard rates) {
    this.rates = rates;
}

Existing callers keep working. Tests use the second constructor. The class is now testable and nothing broke — and this single technique will get you further in a legacy codebase than any amount of arguing about patterns.

Check your work

The correct way to write a singleton in Java: an enum with one constant.

Why the enum form: the JVM guarantees single creation, thread-safely, and it survives serialisation and reflection.

Why singletons are usually wrong: they hide dependencies, make tests share global state, are global mutable state, and bake in a guess that there will only ever be one.

What dependency injection actually is: constructor parameters. A framework automates the wiring; the idea does not need one.

What the composition root is: the one place, usually main, that knows how the pieces fit together.

When a singleton is fine: no mutable state — a namespace of utilities, or an enum constant.

The test to apply: mutable state makes it a global variable; no state makes it a namespace.

What to do in a legacy codebase: stop adding more, and add a constructor taking the dependency while leaving the old one delegating to getInstance().

Practice

  1. Write RateCard as an enum singleton and use it from two classes. Then write a test needing different prices, and see what you have to do.
  2. Rewrite the same thing with constructor injection. Write the same test again.
  3. Look at the capstone's BillingService constructor. List everything it tells you before you read a method body.
  4. Find main in the capstone and identify the composition root.
  5. Add a setPrices method to your enum singleton. Write two tests that pass alone and fail when run together.
  6. Find a singleton in the JDK. Decide whether it holds mutable state.
  7. Take a class of yours that calls a static method for something external. Add the two-constructor bridge and a test that could not be written before.
  8. Argue for a singleton in a real situation. Then say what you would do when a second instance is needed.
  9. Explain why Math is not the problem this lesson describes.
  10. Count the getInstance() calls in an open-source Java project. Pick one and work out what testing it would take.

Next: letting behaviour vary, which lambdas made almost free.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship