Low-level designCore9 min

Dependency injection

An object shouldn't create or look up the collaborators it depends on. It asks for them, usually in its constructor, and one place at the program's entry point builds everything and hands it over.

Watch it evolve1 / 10
The problem

A job that emails overdue notices

OverdueJob asks Loans for overdue loans and has a Notifier email each member at least three days late.

80
Start
Stage 1
Stage 2
Final

1 · The situation

A job that emails overdue notices

A community library runs a nightly job. OverdueJob asks Loans for the loans that are overdue and, for each one at least three days late, has a Notifier send the member a notice. Both are interfaces, with CatalogLoans and EmailNotifier behind them, so Dependency inversion is already in place.

What isn't settled is who creates the collaborators. In the first version the job creates them itself, in its field declarations. Python names the loan field days_overdue; everything else is the same in all four languages.

The first versionJava
import java.util.List;

record Loan(String title, String email, int daysOverdue) {}

interface Loans {
    List<Loan> overdue();
}

interface Notifier {
    void send(String to, String text);
}

class CatalogLoans implements Loans {
    public List<Loan> overdue() {
        return List.of(
                new Loan("The Overstory", "ana@example.com", 3),
                new Loan("Piranesi", "ben@example.com", 10),
                new Loan("Circe", "cy@example.com", 1));
    }
}

class EmailNotifier implements Notifier {
    public void send(String to, String text) {
        System.out.println("email to " + to + ": " + text);
    }
}

class OverdueJob {
    private final Loans loans = new CatalogLoans();
    private final Notifier notifier = new EmailNotifier();

    int run() {
        int sent = 0;
        for (Loan loan : loans.overdue()) {
            if (loan.daysOverdue() < 3) continue;
            String text = loan.title() + " is " + loan.daysOverdue() + " days overdue";
            notifier.send(loan.email(), text);
            sent++;
        }
        return sent;
    }
}

public class Main {
    public static void main(String[] args) {
        OverdueJob job = new OverdueJob();
        System.out.println("Notices sent: " + job.run());
    }
}
«create»11«record»Loan− title: String− email: String− daysOverdue: int«interface»Loans+ overdue(): List<Loan>«interface»Notifier+ send(String, String)CatalogLoans+ overdue(): List<Loan>EmailNotifier+ send(String, String)OverdueJob− loans: Loans− notifier: Notifier~ run(): int

The class diagram of this version, drawn from its code: the job owns its loans and its notifier.

email to ana@example.com: The Overstory is 3 days overdue
email to ben@example.com: Piranesi is 10 days overdue
Notices sent: 2

2 · The change

A test that mustn't send email

The library wants a test for the job's rule: a loan one day late gets no notice. Running the job sends real email, and the job offers no way to give it anything else. The interfaces are there, but the job has already chosen the implementations.

A common next step is a service locator: a global registry the program fills at startup, which any class can ask for what it needs. The job no longer creates anything:

The job asks a registryJava
class OverdueJob {
    int run() {
        int sent = 0;
        for (Loan loan : Services.loans().overdue()) {
            if (loan.daysOverdue() < 3) continue;
            String text = loan.title() + " is " + loan.daysOverdue() + " days overdue";
            Services.notifier().send(loan.email(), text);
            sent++;
        }
        return sent;
    }
}

That works for the nightly run. Then someone writes the test, registers the loans the test needs, and doesn't think of a notifier, because nothing about the job says it needs one:

email to ana@example.com: The Overstory is 3 days overdue
email to ben@example.com: Piranesi is 10 days overdue
Notices sent: 2
Test failed: no Notifier registered

The nightly run works; the test fails at run time, inside run().

How the job gets a collaborator

What a reader of its constructor sees

What a test can do

It creates it

Nothing

Nothing: the real email goes out

It asks a global registry

Nothing

Fill the registry correctly, and reset it after

It's given it

Every collaborator, by type

Pass in a fake

The practice checks flag the first version: the job builds its own CatalogLoans and EmailNotifier. They pass the locator version, which hides the same dependencies better. Its costs are real anyway:

  • Hidden needs. The constructor takes nothing, so a caller learns what the job needs only when it fails.
  • Everything depends on the registry. Every class that asks it is tied to Services, and to it being filled in the right order.
  • Shared global state. Tests that register different services can interfere with each other.

3 · The fix

Ask for collaborators, and build them in one place

Start again from the first version, and turn the job's choices into parameters:

OverdueJob takes a Loans and a Notifier, keeps them in fields, and creates nothing. Its constructor now lists exactly what it needs.

Java
class OverdueJob {
    private final Loans loans;
    private final Notifier notifier;

    OverdueJob(Loans loans, Notifier notifier) {
        this.loans = loans;
        this.notifier = notifier;
    }

    int run() {
        int sent = 0;
        for (Loan loan : loans.overdue()) {
            if (loan.daysOverdue() < 3) continue;
            String text = loan.title() + " is " + loan.daysOverdue() + " days overdue";
            notifier.send(loan.email(), text);
            sent++;
        }
        return sent;
    }
}
«create»«record»Loan− title: String− email: String− daysOverdue: int«interface»Loans+ overdue(): List<Loan>«interface»Notifier+ send(String, String)CatalogLoans+ overdue(): List<Loan>EmailNotifier+ send(String, String)RecordingNotifier− sent: List<String>+ send(String, String)~ count(): intOverdueJob− loans: Loans− notifier: Notifier~ run(): int

Something still has to create the real loans and notifier. That job moves to the entry point, main, the only code that names the concrete classes. It's called the composition root:

The composition root, and the testJava
import java.util.ArrayList;
import java.util.List;

record Loan(String title, String email, int daysOverdue) {}

interface Loans {
    List<Loan> overdue();
}

interface Notifier {
    void send(String to, String text);
}

class CatalogLoans implements Loans {
    public List<Loan> overdue() {
        return List.of(
                new Loan("The Overstory", "ana@example.com", 3),
                new Loan("Piranesi", "ben@example.com", 10),
                new Loan("Circe", "cy@example.com", 1));
    }
}

class EmailNotifier implements Notifier {
    public void send(String to, String text) {
        System.out.println("email to " + to + ": " + text);
    }
}

class RecordingNotifier implements Notifier {
    private final List<String> sent = new ArrayList<>();

    public void send(String to, String text) {
        sent.add(to);
    }

    int count() {
        return sent.size();
    }
}

class OverdueJob {
    private final Loans loans;
    private final Notifier notifier;

    OverdueJob(Loans loans, Notifier notifier) {
        this.loans = loans;
        this.notifier = notifier;
    }

    int run() {
        int sent = 0;
        for (Loan loan : loans.overdue()) {
            if (loan.daysOverdue() < 3) continue;
            String text = loan.title() + " is " + loan.daysOverdue() + " days overdue";
            notifier.send(loan.email(), text);
            sent++;
        }
        return sent;
    }
}

public class Main {
    public static void main(String[] args) {
        OverdueJob job = new OverdueJob(new CatalogLoans(), new EmailNotifier());
        System.out.println("Notices sent: " + job.run());

        // A test hands the job a fake notifier.
        RecordingNotifier recorder = new RecordingNotifier();
        new OverdueJob(new CatalogLoans(), recorder).run();
        System.out.println("Test: " + recorder.count() + " notices recorded, none emailed");
    }
}
email to ana@example.com: The Overstory is 3 days overdue
email to ben@example.com: Piranesi is 10 days overdue
Notices sent: 2
Test: 2 notices recorded, none emailed

4 · The technique

Dependency injection: be given what you need

Dependency injection means an object receives its collaborators from outside instead of creating them or asking a registry for them. The object states what it needs; whoever creates it decides which implementations to supply. Dependency inversion says what the job should depend on, an interface; injection is how the right implementation reaches it.

«create»«record»Loan− title: String− email: String− daysOverdue: int«interface»Loans+ overdue(): List<Loan>«interface»Notifier+ send(String, String)CatalogLoans+ overdue(): List<Loan>EmailNotifier+ send(String, String)RecordingNotifier− sent: List<String>+ send(String, String)~ count(): intOverdueJob− loans: Loans− notifier: Notifier~ run(): int

After the fix: the job holds two interfaces it was given, with a real and a fake notifier behind one of them.

Form

How the collaborator arrives

Use it for

Constructor injection

A constructor parameter, kept in a field

Anything the object needs to work at all; the default choice

Setter injection

A method called after construction

An optional collaborator with a sensible default, or one a framework sets

Method injection

A parameter of the one method that needs it

A collaborator that changes from call to call, such as the current request

The composition root is the single place, as close to the entry point as possible, where the program's objects are created and connected: main in a command-line program, the startup code in a web app. Only applications have one; a library should take its collaborators as parameters and leave the choice to the application that uses it.

5 · When breaking it is fine

Long constructors, wiring code and containers

Injection has costs:

  • Wiring code. Someone writes the new calls, and in a large program the composition root grows long.
  • Long constructors. A class that takes eight collaborators shows every one of them. That's honest, and usually a sign the class does too much.
  • Indirection. To learn which notifier runs, you read the root, not the job.

When the wiring grows, a container can write it: you register which class implements each interface, and it builds objects by reading their constructors. Containers also manage lifetimes: one shared instance for the whole program, one per request or scope, or a new one every time it's asked for.

Container

Language

How it works

Spring

Java

Finds constructors and annotations at run time

Dagger

Java and Kotlin

Generates the wiring code at compile time, with no reflection

The built-in .NET container

C#

Registration at startup, with singleton, scoped and transient lifetimes

None

JavaScript, Python, C++

Plain constructors and a hand-written root are the usual choice

A container belongs in the composition root and nowhere else. A class that receives the container and asks it for services has rebuilt the service locator. And watch the lifetimes: a long-lived object that's given a short-lived one keeps it alive too long. The .NET container, for one, checks at startup that no per-scope service is injected into a single shared one, when scope validation is on, as it is by default in the development environment.

Skip injection for:

  • Values and data. A Loan is created where it's needed; it isn't a collaborator.
  • Stable helpers with no side effects, such as math or string functions.
  • Small scripts that run once, where no test will ever swap anything.

6 · Getting it right

Required in the constructor, built at the root

  • Prefer constructor injection. The object is complete as soon as it exists, and its fields can be final.
  • Inject interfaces, not implementations. Taking an EmailNotifier would let the test pass nothing else.
  • Keep the root at the entry point, and keep it the only code that names concrete classes.
  • Don't pass the container around. Inject what the class needs, not something it can ask.
  • Treat a long parameter list as a design signal. Split the class before reaching for setter injection to hide the length.

Each language has its usual shape:

Language

The usual shape

Java

Constructor parameters stored in final fields; a container such as Spring or Dagger in larger apps

C++

References when the root owns the collaborators and outlives the object, std::unique_ptr when the object owns them

JavaScript

Constructor parameters; a default value such as notifier = new EmailNotifier() is convenient but names the class again

Python

Constructor parameters, often typed with a Protocol; a plain function can be injected where one method would do

7 · Look-alikes

Ways an object gets what it needs

These ideas all touch how objects find their collaborators. They answer different questions.

Idea

The question it answers

Dependency injection

How does an object get its collaborators without creating them?

Dependency inversion

What should the object depend on: an interface its own side owns

Service locator

Where can any object look up a collaborator? It hides what each class needs

Factory

Who creates objects of a family, and how? A factory can be injected, and injection can use factories

Inversion of control

Who calls whom: a framework calling your code; injection is one kind

Strategy

How does an object use one of several ways to do a job? Strategies usually arrive by injection

The checks recognize the fixed version as dependency injection, and also as Strategy: a class holding one of several notifiers, given from outside, has the same shape. The difference is intent. Here the second notifier exists for the test, not as a choice of behavior.

8 · In practice

A garage given its pricing

In the parking-lot practice problem, the lot depends on pricing, and match days change the rates. The checks recognize dependency injection when a class's constructor stores collaborators typed by an interface and the class never creates an implementation of them itself.

A lot that creates its own pricing with new is flagged instead, like the first version here. Design the parking lot with its pricing handed in from main.

Check yourself

4 questions

1. The job needs a clock to decide how late each loan is. Where should the clock be created?
2. Why did the locator version pass the checks but fail its test?
3. A class's constructor takes eight collaborators. What's the best response?
4. Which of these should the job receive by injection?

Takeaways

Remember this

  • An object should ask for its collaborators instead of creating them or looking them up.
  • Constructor injection is the default: the object is complete from the start, and its needs are visible.
  • A composition root at the entry point is the one place that creates and connects the objects.
  • A service locator hides what each class needs; the failure shows up at run time.
  • A container is optional; when used, it belongs in the composition root alone.