Briebug Blog

Sharing our thoughts with the community

Intro to Angular Dependency Injection

Intro to Angular Dependency Injection


One of the great features built into Angular is its dependency injector. An Injector like the one included with Angular supports rich, configurable Inversion of Control, which is a key principle of software design that improves quality, flexibility, testability and maintainability. If you have used Angular and written at least one service, then you have used dependency injector already, although you may not have explicitly known about it. This article will introduce you to more of the Angular Injector's concepts and capabilities.

Inversion of Control

Before we delve into the Angular Injector, let's first talk about Inversion of Control, or IoC for short. This is a key software design principle that aims to improve the nature of coupling in software. Coupling, how closely associated and dependent code is, is a key factor in software quality and in particular long-term maintainability and viability.


Tight coupling outside of highly cohesive units, such as within a class, is generally a negative trait, whereas loose coupling is a more positive trait. Looser coupling improves the ability of software to change over time as requirements adjust to real-world demands. Higher coupling creates rigidity that can be extremely challenging to overcome. Reducing coupling between discrete units of code is one of the things IoC facilitates.

Inversion?

So why is it called an inversion of control? When it comes to dependencies, if you have been programming for some time especially in object oriented languages, you may have come across "normal" dependency control. This is where a dependent will directly instantiate (i.e. "new up") its dependencies.


In other words, the dependent is in direct control over its dependencies.


We use a classic Angular scenario as an example here. Most Angular applications have components, which consume services, which in turn may consume more services, etc. We may have a ui component consuming a facade that consumes a couple of data services, each of which consume an http service, which itself may need to consume other services (say an encoding service).


Without IoC or a dependency injector, each component would be responsible for constructing its own dependencies.

Looks pretty straightforward. Each component is responsible for its own dependencies, and new them up as necessary in their own constructors. Seems like an amicable situation....until you need dynamic control over how each dependency is instantiated.

Dynamic Dependencies?

What if, in some cases, you need the ApiClient to use a FastEncoder and in other situations you need it to use a SecureEncoder? How do you accommodate that need? Maybe you update the constructor with a flag to determine whether to use a "fast" or "secure" encoder:

Simple enough, right? Well, what if you eventually find a way to combine speed and security into a FastSecureEncoder down the road? What if after that, you end up creating an even more effective, more cryptographically secure, and much faster encoder?


More significantly, what if the ApiClient is not the only dependent of the encoder? Every time you create a new, improved encoder, you will need to find every dependent, and update each of them to create the new dependency. In a large scale application, consider that in some cases, you could very well have hundreds, if not thousands of dependents for any given dependency... Managing that can rapidly become a significant time and cost sink.


The solution to these problems? Invert Control!


Instead of allowing each dependent to directly control their own dependencies, allow some external facility to control what instances of what dependencies are actually associated with each other.

Testability

Another issue that arises with direct dependency management occurs when trying to unit test your code. When dependencies are created directly by dependents, there is no way to replace them. If you test one component or service, you test everything it depends on as well. Those are integration tests, and have value in and of themselves, however even integration tests may need to create a boundary of scope for each test case in many cases.


When it comes to unit testing, the goal is to isolate the unit under test, and that requires the ability to replace real dependencies with fake ones. This becomes impossible when dependencies are managed directly by their dependents. For unit testing to be possible, we need a way to control the actual instances that are created for each dependency. We need to be able to fake dependencies with test fakes, stubs and mocks.


The solution to this problem is once again to: Invert Control! This allows us to construct components, services, etc. using an external actor (possibly test code, or another facility for normal runtime which we will go into more detail here in a moment) from pools of various instances of these things that are created and ready to be used, and even reused.

The Container

So, how exactly do you solve this dependency problem? The principle is to Invert Control, or "Inversion of Control", by taking away the responsibility of 'newing up' specific dependencies from each dependent, and giving that control to something else... What else, though?


The classic term for it is a Dependency Injection Container. In Angular, you may come across the Injector, which is the tool you may sometimes use to explicitly inject, or acquire references to, dependencies. You are most likely to have come across this when unit testing Angular components and services. However fundamentally, the injector is finding dependencies, also called "providers", in a "container" of dependencies.


The container is where dependencies can be defined and configured, and where instances of them may be created and may reside long term, for use by their dependents.

Inverted

If we take our scenario from before, but invert the management of their dependencies, we would no longer be locked into one particular kind of any dependency. We wouldn't be locked into just Encoder for the encoder dependency of the ApiClient. We could choose when to inject different kinds of encoders.

Note the key difference here. We are constructing each part of our app independently, then composing larger scale components from smaller ones, until we have, in essence "assembled" our entire app. The app is composed of other parts...components, facades, services, etc. This approach gives us complete and total control over our dependent->dependency relationships, and allows us to configure alternative implementations as we see fit when we see fit, without having to modify the code of every single dependent whenever such an alternative needs to be introduced.


We now also have the opportunity to swap out components as necessary. We can now easily change out the encoder implementation as we see fit:

At its most basic, providers are simple configurations that describe what to provide when a dependency is required. For most dependencies in the average Angular project, providers are simply the classes you are providing themselves! Nice and simple in the general use case.

Custom Provider Configurations

Angular's injector supports much richer configurations supporting more advanced dependency injection when necessary. Providers are most often defined "shorthand", by configuring the service to be used as the provider itself. This is the most common and simple use case, and is demonstrated above, and you have likely already configured providers this way yourself if you have used Angular before.

InjectionToken

A provider itself is really just a "token", an identifier by which a dependency may be identified, found, resolved. Providers may also be configured to actually construct a different service than that being used as the provider. Further, providers may also use custom InjectionToken instances to abstract the provider from the concrete implementation that provider constructs.


An InjectionToken is just that...a token. It is basically a class, that wraps a string, which itself is just an identifier. If necessary (rather rare, and out of the scope of this article), an injection token may also accept some basic configuration, which is beyond the scope of this article. Creating and using an injection token is easy:

In this simple example, we have created an injection token called Encoder, and used that token to provide the FastEncoder version of our encoder. To use this kind of provider, we must be a little more explicit when injecting the dependency. As an example, our ApiClient would need to be updated like so:

The @Inject() decorator allows us to explicitly specify exactly which provider we want to use, in order to inject the required dependency represented by that parameter. Without @Inject(), angular's Injector will try to look for a provider that matches the specified type for the parameter. Here, however, our type is IEncoder, a typescript "interface".


When typescript code is compiled, all interfaces are stripped from the resulting javascript code (as they are not first-class features of javascript and cannot be represented), so there is actually nothing by which Angular could look up the encoder by. This also means that interfaces can never be used as providers...only classes, or InjectionToken can be used as providers. This is Angular's solution to get around some of the shortcomings of JavaScript, which TypeScript transpiles into.

Configuring Providers

Providers may provide simple values (useful for injectable configuration, most often used when creating configurable Angular libraries), may provide instances of a class, or may use a custom factory function that allows you, the developer, to control exactly what is created and how.


For clarity, the simple form of providing a dependency, by just specifying the class name, is equivalent to (shorthand for) this configuration:

You may have configured providers before, especially if you use any of Angular's internal configuration injection tokens, or perhaps third party libraries that allow custom configuration. Customizing a provider is quite easy. Lets say our secure encoder requires a Crypto dependency, and lets assume our crypto dependency is configurable:

Here, we have used a simple provider for our Crypto class, a value-type provider to configure the Crypto service, and a factory-type provider to control how we create the encoder. We even use the @Optional directive in our encoderFactory to mark the crypto dependency as optional. The factory will then determine which encoder to create based on whether the crypto dependency is available or not.


One of the key reasons to use a factory provider in Angular is to take direct control over how and when an instance of a dependency is constructed. Where a value or class will simply provide a basic version of the dependency, a factory gives control over the specifics of how what kind of dependency is constructed, how it is constructed, and even what dependencies for the dependency being created by the factory are used, if any.


Note that for the crypto configuration, we have used a simple value, rather than a class or factory. Value providers are an excellent way to externalize configuration settings, allowing dependencies to be configured on a use case basis.

Additional Configuration Options

In addition to class, value and factory providers, Angular also supports two other kinds of providers: multiple providers and aliased providers. It is possible to alias providers, via the useExisting option, which allows us to reuse an existing dependency under a different provider token.

You may find useExisting is beneficial when you may need to replace existing services with new services, that may be interface-compatible, but contain different implementations. An old provider (say a class) could be used to provide the new service to both old and new consumers, allowing progressive and "lazy" refactoring to eventually switch all old provider references to the new. Another use case is when you may in fact need to use different providers (which are just tokens to look up a dependency) to find the same dependency.


Another type of provider is a multiple, or multi, provider. These allow the same provider token to be used to provide more than one thing, and allow those things to be provided independently at different times in different places even. Such providers will inject an array of all the provided dependencies configured:

You may find that multi providers are useful when you need to provide extension points for your code. This is most common when implementing extensible or pluggable libraries, but you may also find use cases in your own application code bases. This can be particularly true with Nx monorepos, where heavy library use is a part of the standard design of your applications. Multi providers can expose extensibility and customizability points into your libraries.

The Injector

The injector is responsible for utilizing providers to resolve dependency instances and inject them into dependents! Again, Angular ships with a built in dependency injector. In fact, dependency injection is so endemic to Angular, that it happens automatically most of the time! Angular's injector is quite configurable and supports a variety of ways to provide instances of dependencies.


Every @Injectable() class is one that can have dependencies injected into it. If a class is not decorated with this decorator, then Angular's injector will be unable to inject dependencies into it. Not that this decorator does not mean the class can be injected, it actually means that it can have dependencies injected into it (small, but important, distinction, with one small exception we will cover soon.)

Hierarchical 

At its most basic, an instance of the Angular injector is created for each module. When a module is imported into another module, the injector for that imported module is linked with the importing module's injector as the parent injector. This ensures that anything within the imported module can inject any dependency defined either within that child module, or from any parent module in the chain, up to the very root injector of the entire app.


It should be noted that, if there are two modules that are effectively "siblings" (i.e. they are both imported into the same parent module, say your AppModule), a provider defined only in one of those child modules will NOT be available within any of its siblings. Only providers accessible within the parent chain of injectors from the current module up are available.

Overriding Providers

If we had another module, or even a specific component, that needed to override the encoder and one or more of its dependents only in that scope, we could accomplish that like so:

A couple things to note here. If you are going to override a dependency of some particular service or component...in this case, the encoder that is a dependency of the ApiClient, you must provide both the overridden dependency, as well as the dependent, in each scope. This is why we have provided both an alternative encoder, as well as the ApiClient itself, in both MyModule as well as MyOtherComponent.


This is necessary, as otherwise, if anything within MyModule tried to inject ApiClient, without overriding that dependency within the module, the instance created for AppModule would be used instead. Note that dependency instances created at each scope are "singletons" which we will get into in a moment. The issue there is that the instance created for AppModule uses the basic FastEncoder, rather than SecureEncoder.


Also note that we have overridden the encoder at the component level for MyOtherComponent. By providing the encoder and the ApiClient there as well, whenever that particular component is constructed, it will use its own instance of the ApiClient, which in turn will use the locally provided instance of the encoder, which is FastSecureEncoder. Any other dependencies the ApiClient requires will be provided from parent injectors of the local injector.

Lifecycle

As mentioned above, the Angular injector is responsible not only for managing the creation of instances for you, but it also manages the lifecycle of those dependencies. By default, instances created by the injector are "singleton" instances. That means a single instance of the dependency for that provider is created, and that same single instance is shared among all dependents. Any time a dependent is constructed by the injector, the same single instances of its dependencies are provided unless the "lifecycle" is otherwise changed.


To take control over dependency lifecycle, you can either define another provider in a child scope, which will use the child injector to manage the lifecycle of that provider (again by default, as a singletone), or use a factory. When using a factory to create dependency instances, you are in complete control over the lifecycle of the dependencies.


By default, a factory is executed each time its provider is used, however since you are in control of the implementation of a factory, you can choose how to manage instances. You can memoize the result, and effectively create a singleton. You could memoize the result based on the dependencies passed to the factory, which could create a memo per "configuration" of the dependency. You could also implement completely custom lifecycle control however you see fit.

Manual Injection

Angular's Injector is itself a service that is provided through the dependency injection system. This means you may inject Injector into your own services and components, and use it to manually inject dependencies at runtime. In contrast to normal dependency injection, which is more of a static thing fixed at compile time (with some small exceptions), using the Injector allows truly dynamic dependency injection at runtime.

Using the Injector Service

To use the Injector service, simply inject that into your dependent. From there, you should use the .inject() method to retrieve a reference to the desired dependencies.

With the above simple ServiceLocator utility service, we use the Injector and a simple TypeScript "map" to translate from string names for services, to either the service type or injection token required to inject that service. Since the Angular Injector requires that all injections be performed through either types or InjectionTokens, this is a handy pattern when you need to look up services by some other kind of token such as a string in this case.

Automatic Provisioning

Before we conclude this article, there is one final aspect of the Angular dependency injection system that we should cover. That of automatic provisioning via the providedIn configuration option of the @Injectable() attribute. Since Angular 6, this alternative approach to providing common services has become the standard approach when basic provisioning is all you require.


Previously, it was noted that decorating a class with @Injectable() was not really about making a class capable of being injected elsewhere, but about allowing dependencies to be injected into itself. This is fundamentally true, and is the primary purpose of the decorator. However, the decorator was expanded to accept configuration options for the decorated class, thus exposing the providedIn option.

Providing in root

The default and most common use of providedIn is with the value 'root'. This was the original option provided in Angular 6, alongside the ability to specify a specific module class. When providing an injectable in root, you are instructing Angular to determine on its own exactly where to provide your class.


There can be two possible outcomes with this option. The first, where a single module depends upon the service, will result in the class being provided within that module. If for any reason more than one module depends upon the service, it will in fact be provided in the application's root injector. This be the first module your application loads, which in most applications will be app.module.


This behavior ensures that the dependency is provided as a singleton throughout the application.

Providing in any

The next method of using providedIn is with the value 'any'. This option was added in Angular 9, and allows multi-module provisioning except in an edge case. It is useful when you need to make sure that each dependent module gets its own instance of the dependency, which may be necessary if each module defines alternative child dependencies of the 'any' dependency.


Providing in 'any' has a key edge case, and that is for eagerly loaded modules. Starting in Angular 10, eagerly loaded modules (those imported directly into your application module, as well as any other modules those modules import, etc.) will all share a single instance of dependencies provided with 'any'.


Since Angular 10, if a module is not eagerly loaded, it is either lazily loaded or preloaded (which is technically proactively lazy, and thus still lazy). All lazy loaded modules will be provided their own instance of 'any' dependencies.

Providing in platform

Also since Angular 9, a third option is defined for providing dependencies. This third option, 'platform', is a special case. It allows dependencies to be registered at the Angular platform level, which is the parent of the root level of the current app.


Dependencies provided in 'platform' are truly global singletons and will be shared across different Angular instances. This approach to provisioning can be useful if you must execute more than one Angular application on a page (perhaps some kind of micro frontend approach, web components that may be used on third party pages, etc.)

Providing in a specified module

The last option for provisioning with the @Injectable() decorator is to specify a module class type. This will provide the dependency directly within that module. This option is often problematic, as it can potentially create circular references, where a service depends on the module, while the module depends on other dependents that in turn may depend on your service.


It is recommended that if you wish to provide a dependency directly within one or more modules, you provide them within those modules using the @NgModule providers configuration.

Tree Shaking

One of the benefits of using providedIn in general is that if a service is never actually consumed, or if an old service ceases to be consumed, and is not explicitly provided in a module, and thus never referenced anywhere...it will not be included in your bundles. This ensures that services are "tree shakeable".


Tree shakable services, and just services provided with either root or platform help ensure that your bundles are optimal in terms of size, as the most efficient place to register your services will be used.

Dependable!

Well, that's a wrap for Angular's dependency injector! Simple on the surface, with most provisioning being done automatically these days, the Angular Injector is a very powerful tool in the toolbox of any web developer. It is in fact so powerful, that it has been replicated in other platforms, including React through Thomas Burleson's Mindspace Utils, a library of powerful hooks that include a useInjectable.


Inversion of Control is a key architectural tenet of quality software design, especially at enterprise scale, where you may have to manage thousands of dependencies and may require that those dependencies be very loosely coupled and highly configurable.


When you need control over your dependencies, the Angular injector exposes an API for configuring dependencies in many ways. Dependencies may be redefined in differing scopes (i.e. different modules, even for specific components). Dependencies may be provided with opaque InjectionTokens rather than the dependencies themselves, decoupling the token used to find the dependency from the actual dependency being provided.

Need Support With a Project?

Start experiencing the peace and security your team needs, and continue getting the recognition you deserve. Connect with us below. 

First Name* Required field!
Last Name* Required field!
Job Title* Required field!
Business Email* Required field!
Phone Required field!
Tell us about your project Required field!
View Details
- +
Sold Out