Angular Dependency Injection

Dependency injection (DI) is one of Angular's defining features. Components and services declare what they need (an HTTP client, an auth service, configuration), and Angular's injectors create and supply those dependencies. That keeps classes decoupled from how their dependencies are built, makes swapping implementations trivial (real vs mock, browser vs server), and gives you precise control over scope: one app-wide singleton, one instance per feature route, or one per component.

Modern Angular favors the inject() function over constructor parameters, providedIn: 'root' tree-shakable services, and functional providers (provideHttpClient(), provideRouter()) instead of NgModule configuration. The underlying concepts, providers and the injector hierarchy, remain the key to using DI well.

TL;DR

Quick Example

Core Concepts

Injectable Services and providedIn

@Injectable({ providedIn: 'root' }) registers the service with the root injector lazily and tree-shakably: if nothing injects it, it's dropped from the bundle. The result is one instance shared across the app. Use providedIn: 'platform' rarely (shared across multiple apps on a page), and plain @Injectable() when the service will be provided explicitly somewhere.

The inject() Function

inject() works in an injection context: field initializers, constructors, provider factories, route guards and resolvers, and functions called from them. It enables functional guards, interceptors, and reusable helper functions (injectQueryParam() style), and it works better with inheritance than constructor injection. Outside a context, use runInInjectionContext or pass an Injector.

The Injector Hierarchy

When a dependency is requested, Angular searches:

  1. Element injectors, starting at the requesting component or directive, walking up the component tree through providers and viewProviders.
  2. Environment injectors: route-level providers for lazy-loaded routes, then the root (application) injector, then the platform injector.
  3. Otherwise it throws NullInjectorError.

The first provider found wins, so providing a service at a component level shadows the root instance for that subtree. Resolution modifiers: { optional: true }, { self: true }, { skipSelf: true }, { host: true }.

Provider Recipes

InjectionToken

TypeScript interfaces don't exist at runtime, so they can't be DI tokens. new InjectionToken<T>('description') creates a typed token for configuration objects, functions, feature flags, or interface-based abstractions. Tokens can include a providedIn: 'root' with a factory for defaults.

Environment and Route Providers

DI in Testing

Override providers per test with fakes, and use TestBed.overrideComponent for component-level providers. Because components depend on abstractions supplied by DI, most tests need no real HTTP or browser APIs. See mocking and stubbing.

Best Practices

Default to providedIn: 'root' for Stateless Services

API clients, loggers, and utility services should be root singletons, which are tree-shakable and simple. Provide at component or route level only when you need a separate instance or scope.

Use Component Providers for Local State

Stores tied to one instance of a UI (an editor, a wizard, a table with its filters) belong in that component's providers, so each instance gets fresh state that's destroyed with it.

Prefer inject() in New Code

It reads cleanly, works in functional guards, interceptors, and resolvers, and avoids long constructor parameter lists in subclasses. Angular provides a migration schematic from constructor injection.

Model Configuration With Tokens

Inject configuration through typed InjectionTokens rather than importing environment files everywhere. It makes code testable and environment-agnostic, and it works with SSR.

Common Mistakes

Providing a Service in Multiple Places Accidentally

Listing a root service in a component's providers creates a separate instance for that subtree, and suddenly "shared" state isn't shared. Provide it once, deliberately.

Calling inject() Outside an Injection Context

Inject in field initializers or the constructor, or use runInInjectionContext(this.injector, …).

Using Interfaces as Tokens

inject(PaymentGateway) where PaymentGateway is a TypeScript interface fails, because interfaces vanish at runtime. Use an abstract class or an InjectionToken<PaymentGateway>.

FAQ

What does providedIn: 'root' mean?

The service is registered with the application's root injector, so there's one shared instance for the whole app, created lazily on first injection. It's also tree-shakable: unused services are removed from the production bundle.

Should I use inject() or constructor injection?

Both work, and they're equivalent at runtime. inject() is the modern recommendation: it works in functional APIs (guards, interceptors, resolvers), simplifies inheritance, and keeps constructors clean. Constructor injection remains fully supported.

How do I get a separate service instance per component?

Add the service to the component's providers array (and don't use providedIn: 'root' for it). Each component instance then gets its own service instance, shared with its children and destroyed with the component.

How does Angular DI compare to Spring's?

Conceptually similar: containers create and wire objects, singletons are the default, and scopes and qualifiers exist (via the injector hierarchy and tokens). Angular DI is hierarchical along the component tree, which gives natural per-component scopes, while Spring focuses on application-context beans. See Spring dependency injection.

Related Topics

References