Home Design Patterns Dependency Injection

Dependency Injection

Modern Practice

Hand a class its collaborators instead of letting it construct or find them.

Purpose

A class that writes new SqlJobStore(dsn) inside its own constructor has made a decision that does not belong to it. It has chosen which database library the program uses, which configuration key that library reads, and which class every test of it must also drag along. Dependency Injection moves that decision outward: the class states what it needs, and whoever assembles the program decides what to supply.

The name is heavier than the idea. Ninety percent of the pattern is a constructor parameter and a field to keep it in. The remaining ten percent is the discipline of never reaching for a collaborator by any other route: not new, not a static singleton, not a global registry, not a service locator. If a class can only be reached one way, then the way it is reached is part of its design, and the design is now yours to choose rather than the class's to impose.

Design

There are three places to inject, and they are not equal. Constructor injection takes the collaborators as constructor parameters and stores them in fields that are never reassigned. It is the one to reach for by default, because it makes an incompletely wired object impossible to create: if the program compiles and the object exists, everything it needs is present. That is a real invariant, and you get it for free.

Setter injection assigns the collaborator after construction. It is what you fall back to for genuinely optional collaborators and for dependency cycles, and it costs you the invariant: now every method has to tolerate the field being unset, and "unset" is a state that exists only because of how the object is wired. Method injection passes the collaborator to the single call that needs it, which is right when the collaborator changes per call rather than per object.

UML class diagram: a composition root creates ServiceImpl and passes it into Client's constructor; Client depends only on the Service interface, which TestDouble also implements.
Follow the two dashed arrows out of the composition root. That box is the only one in the picture allowed to name a concrete class, and pushing every such name into it is the whole point of the exercise.

Which raises the question the pattern is really about: if no class creates its own collaborators, who creates anything? The answer is the composition root, one place per deliverable, normally main or a framework's start-up hook, which knows every concrete type and builds the object graph once. Everything below it is written against interfaces. Concrete-class knowledge stops being spread over four hundred files and becomes one file you can read.

A DI container is a different thing wearing the same word. A container inspects constructor signatures, usually by reflection, looks up a registered implementation for each parameter type, and builds the graph for you. Spring, Guice and the .NET service collection all work this way. That saves you the wiring code, which matters once the graph is hundreds of types deep. What it costs is that the wiring is no longer code. A missing registration is not a compile error any more, it is a start-up exception, or on a bad day a runtime exception on the first request that happens to need it.

Example

The Autobot Robotics task scheduler had a JobRunner that built everything it needed in its own constructor: a serial link to the arm from an address in the config file, a PostgreSQL job store from a connection string, and a logger from a static instance. Nothing about that is unusual and it worked fine in production. The problem was that testing JobRunner required a real arm, a real database and a real config file, so in practice nobody tested it. The retry logic, which is the part most worth testing, could only be exercised by unplugging things.

What forced the change was an engineer running the scheduler's one end-to-end test on a bench machine whose config file still pointed at a cell on the production floor. A real Arm802 moved, with a fixture in the gripper. Nobody was hurt and nothing was damaged, but the test could not have done anything else, because the address it connected to was decided inside the class under test.

JobRunner now takes an ArmLink, a JobStore and a Clock in its constructor. The clock was an afterthought that turned out to matter most: the retry tests used to take twenty minutes of real waiting and now take milliseconds against a fake clock, which is the difference between a test suite that runs on every commit and one that runs when somebody remembers. There are two composition roots, and they are deliberately different. The office reporting service uses a container, because its graph is large and mostly boring. The embedded controller wires everything by hand in about sixty lines at start-up, because the firmware has no reflection, allocation after boot is not allowed, and being able to read the entire object graph in one file is worth more there than saving sixty lines.

// Before. The constructor decides which arm and which database.
class JobRunner
{
    public JobRunner(Config cfg)
    {
        this.arm   = new SerialArmLink(cfg.armPort);
        this.store = new SqlJobStore(cfg.dsn);
    }
}

// After. The collaborators arrive already made, and the types are interfaces.
class JobRunner
{
    private readonly ArmLink arm;
    private readonly JobStore store;
    private readonly Clock clock;

    public JobRunner(ArmLink a, JobStore s, Clock c)
    { arm = a; store = s; clock = c; }

    // pull a job, send the motion, retry on timeout using clock
    public void runNext() { }
}

// The composition root: the only place that names a concrete class.
class ControllerMain
{
    public static void main(Config cfg)
    {
        JobRunner runner = new JobRunner(
            new SerialArmLink(cfg.armPort),
            new SqlJobStore(cfg.dsn),
            new SystemClock());
    }
}

class RunnerTest
{
    public void retriesOnceThenGivesUp()
    {
        // No arm, no database, no waiting.
        JobRunner r = new JobRunner(
            new FakeArmLink(), new InMemoryJobStore(), new FakeClock());
    }
}
The pattern is the three constructor parameters and the three readonly fields. Everything else follows from them, including the fact that RunnerTest needs no framework to build a JobRunner that cannot touch anything real.

Seen in the Wild

Spring is the framework that put the term into common use, and constructor injection into a Spring bean is the reference implementation of the container version. ASP.NET Core ships a container in the box: services are registered on IServiceCollection at start-up and controllers receive them as constructor parameters. Guice does the same for plain Java, and Dagger 2 does it by generating the wiring code at compile time instead of resolving it by reflection, which is why it is the one used on Android where reflection and start-up cost hurt. Angular has an injector at the center of its component model. Outside any framework, every constructor in the standard libraries that takes the thing it will work on, such as new BufferedReader(reader), is the same pattern with none of the vocabulary.

Comparisons

  • Singleton is the pattern Dependency Injection most directly displaces. A singleton is a dependency that does not appear in any signature, so a class can acquire one without anybody reviewing the change. If you genuinely want one instance, create one in the composition root and inject it everywhere; you get the single instance without the global reach, and the fact that a class uses it becomes visible in its constructor.
  • Abstract Factory and Factory Method answer "how does this get built". DI answers "who decides which one". They combine constantly: inject a factory when the collaborator has to be created later, or per request, rather than once at start-up.
  • Strategy says an algorithm should be an object the context is handed, and pointedly does not say by whom. DI is the answer to the question Strategy leaves open, and the same is true of Bridge, whose Implementor has to arrive from somewhere.
  • Null Object is what you inject when the honest answer is "nothing", and it is what keeps optional collaborators from needing setter injection and a null check.
  • Proxy becomes almost free once dependencies are injected: the composition root can hand over a caching or logging proxy instead of the real implementation and nothing downstream notices.

Why It Exists

My take

Dependency Injection exists because new is a hard-coded reference in the middle of otherwise abstract code, and because it is invisible. A class's signature tells you what it accepts and what it returns, and tells you nothing at all about what it will reach out and touch while running. That asymmetry is the whole problem. Two classes with identical public interfaces can differ by one of them opening a socket, and you cannot tell by reading their declarations. Injection makes reaching-out part of the signature, which turns a hidden property into a reviewable one.

I rate this above most of the Gang of Four catalog for daily work, and the reason is not flexibility. It is that a class whose collaborators all arrive as parameters can be instantiated in a test, in a tool, in a REPL, in a simulator, by anybody, with no environment. Testability is usually presented as the benefit of DI. I would put it more strongly: testability is a proxy measurement for whether a class has an honest boundary, and the reason injected code is easier to test is that its boundary is real. Code you cannot construct without standing up half the system is telling you something true about its coupling, and the test difficulty is the messenger.

On containers I am less enthusiastic than the industry. A container trades compile-time clarity for configuration-time magic, and that is a genuine trade rather than a free win. Hand-wired composition roots fail at compile time, are navigable with jump-to-definition, and can be read top to bottom by someone who has never seen the framework. Container-wired graphs fail at start-up if you are lucky, are not navigable, and require knowing the registration conventions before the code makes sense. Below roughly fifty types, wiring by hand is shorter than the registration code would have been. Above that, containers earn their place, mostly because of scoped lifetimes: getting one instance per web request, disposed at the end of it, by hand, is genuinely tedious and easy to get wrong.

The place it surprised me most was firmware. An embedded controller has no container, no reflection and no dynamic allocation after boot, so DI there looks like passing pointers to statically allocated structs into an init function. That is the entire pattern, and it is what let us build the same motion code for the target and for a host-side simulator, swapping only the driver at the composition root. The pattern survives the removal of every framework that is usually sold alongside it, which is a good sign that the framework was never the idea.

Criticisms

Dependency Injection makes wide constructors comfortable, and wide constructors are a design problem it is very good at concealing. A class with eight injected collaborators is a class doing eight things, and before DI the pain of building it would have made that obvious. Now it is nine tidy lines in a composition root, or nothing at all if a container is filling them in. Treat the parameter count as the smell it is: the fix is to split the class, not to bundle the parameters into a settings object so the list looks shorter.

Containers add failure modes that the plain pattern does not have. The object graph exists nowhere in the source, so answering "which implementation did I actually get" means reading registration code, conventions, and possibly configuration files, in that order. Lifetime mistakes are the classic bug: a singleton-scoped service that captured a request-scoped one keeps the first request's object alive for the life of the process, which produces cross-request data leakage and is nearly impossible to reproduce in a debugger. Reflection-based resolution also costs start-up time and defeats trimming and ahead-of-time compilation, which is exactly why Dagger generates code instead.

The pattern also invites an interface for every class purely so it can be injected, which doubles the file count and produces the very common sight of an interface with a single implementation and a name that is the implementation's name with an I in front. If there is one implementation and no prospect of a second, inject the concrete type. You still get the seam at the composition root, and you can extract the interface on the day the second implementation actually exists.