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
- Mark services with
@Injectable({ providedIn: 'root' })for tree-shakable app-wide singletons. - Get dependencies with
inject(Service)in field initializers or constructors (injection context only). - Injector hierarchy: environment injectors (root, then lazy routes) and element injectors (component and directive providers).
- Component-level
providerscreate one instance per component instance, which is useful for local state. - Use
InjectionTokenfor non-class dependencies (config objects, functions, interfaces). - Provider recipes:
useClass,useValue,useFactory,useExisting. Override them in tests withTestBed.
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:
- Element injectors, starting at the requesting component or directive, walking up the component tree through
providersandviewProviders. - Environment injectors: route-level providers for lazy-loaded routes, then the root (application) injector, then the platform injector.
- 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
bootstrapApplication(..., { providers })configures the root environment injector.- Route
providerscreate an environment injector for a lazy route subtree: services scoped to a feature area, instantiated when the route loads. makeEnvironmentProviderspackages library setup intoprovideX()functions, the modern replacement forModuleWithProviders.forRoot().
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
- Angular — The framework overview
- Angular Components — Component-level providers
- Angular Routing — Route-scoped providers and functional guards
- Angular Signals — Signal-based stores in services
- Spring Dependency Injection — DI in the Java world
- Design Patterns — Dependency inversion and factories