Angular dependency injection: why?

At work we teach and consult on various topics, notably (for this post) Angular. We are often asked why Angular has and heavily uses dependency injection. Here are my answers to this question; I haven’t had a chance to compare notes with the rest of our Angular expert team, so here it is, on my personal blog. (However, special thanks to Paul Spears, who helped clarify these ideas.)

Dependency injection has a considerable complexity cost, so it’s important to have good reasons to add this complexity to a framework or application. There are an unlimited number of potential features and patterns to follow, so the default for any particular feature or pattern must be “no”. Moreover, popular Angular alternatives (like React) thrive without a dependency injection system, constituting an “existence proof” that it is not a necessity, but rather a choice.

There are a variety of potential reasons to say “yes” to DI, categorized here.

General software engineering reasons

Dependency injection far predates Angular, and is in common use among the (fast, long-term) community of Java Spring developers (among many others). Discussion and reasoning around dependency injection (and related concept, Inversion of Control) are widely available, here are a few starting points.

Angular team reasons

Angular provides access to various capabilities via dependency injection. If you code needs access any of these things, it get access via the dependency injection mechanism. In other words, the Angular team has chosen to use dependency injection therefore your application must use dependency injection. Of course this is something of a tautology. The underlying question is: but why did they do it this way?

To answer that, the Angular team has supplied an explanation of the DI pattern and why they think it is a good idea. Rather than repeat all that here, their reasons are worth a read, and overlap what I have written below.

Polymorphism for testing

Angular developers often talks in terms of services; injecting services, using services, coding services. Although there are additional meanings to the word service beyond the word class, service is often used as a synonym for class, or even as a synonym for “file with some code in it” among the Angular community.

It is common for a unit (or integration, or even E2E) test to need a different implementation of a class, then that which is used during normal operation. DI provides a convenient and flexible way to provide such alternative testing implementations.

Of course, the React community gets by without this, and still is able to perform testing. Typically this is done by treating the JavaScript module system (CommonJS “require”, or the newer “import”) as if it were a configurable DI system, with special machinery to override the meaning of an import during testing. Conveniently, this is done without any syntactical or semantic overhead during normal non-testing operation – a fairly compelling feature. Still, DI provides additional flexibility, configurability, and a more explicit means of achieving polymorphism for testing.

Polymorphism for environments

Sometimes an application needs a different implementation of the same interface (or “service”) depending on the environment in which it is deployed. Dependency injection handles this nicely and explicitly. For example:

  • An application might be compiled for both web use and Electron use, with certain service implementations swapped out.
  • An application might be hosted locally versus in the cloud service, and use a different data backend implementation (of the same Angular service interface presented to the UI).
  • An application compiled for Universal (server-side rendering) use, commonly needs certain bits swapped out during server compilation/execution.

Transitive injection

Code which itself relies on a service available via dependency injection (for example, HTTP) will need to be injected so that it can receive its own injectables.

Instance control

Without a dependency injection system, it is still straightforward to implement either a set of functions (that is, not a class at all, no state), or a singleton class instance, or a class instantiated for each use. But the Angular dependency injection system can achieve much more sophisticated instance control. For example, by configuring providers, a service could be shared by portions of a component tree, but not by the entire application. This sort of clever DI configuration can replace extensive discussion and code with just a couple of lines of configuration.

Reminder: don’t always use services

As we teach classes, and discuss code “in the wild”, developers innocently ask:

“I have some code that doesn’t go in a component, where do I put it?”

Unfortunately, when this question comes up in a Angular context, the obvious answer is usually “put it in an Angular service”. This answer is convenient, especially for teaching how to use Angular features. Unfortunately, this answer is also a lousy default.

The default answer of where to put code should be: in a source code file, in a function or class etc., which are not in any particular way connected to a web application framework by Angular. Only move the code to a Angular-related construct when the need to do so comes up. In a mature, complex application you may find that a sizable portion of the overall code lives outside any Angular construct, outside a Component or Service or NgModule, simply sitting as TypeScript code in source files.

Code should be moved from an ordinary source code file to a Angular service only to meet these needs:

  1. The code relies on some other service that will be injected to it (for example HTTP).
  2. The code will be swapped out for testing.
  3. The code will be swapped out for different environments.
  4. The code has state, in the instance of that state should be tied to the lifecycle of a Angular construct (module, component, etc.).
  5. The code is example code, for the purpose of teaching how Angular service or DI works.