How Angular Dependency Injection Resolves Services with inject()
Understand how Angular DI supplies services through inject(), providers and hierarchical injectors, and how to scope instances and avoid missing-provider errors.
This article is published in English.
Once you start moving logic out of Angular components into services, a practical question appears: when a component writes inject(UserService), where does that object actually come from, who constructed it, and which instance does the component receive? The answer is Angular's dependency injection (DI) system. This guide walks through what DI is, how inject() and providers work, how hierarchical injectors decide which instance you get, and how to avoid the most common beginner error, so you can confidently share or scope services in your own applications.
Here is the line that usually raises the question:
userService = inject(UserService);
Nothing in the component creates UserService, yet the field ends up holding a ready-to-use object. Angular's DI system supplied it.
Why components should not construct their own services
The obvious alternative is to build the service yourself inside the component:
userService = new UserService();
It works, but it hard-wires the component to one concrete class and to one way of building it. The component now decides how the service is constructed, what it needs in order to exist, and whether a fresh copy is created every time. If the service later needs its own dependencies, every place that calls new has to change too.
DI flips that responsibility. The component only declares that it needs UserService. Angular locates a matching provider, creates the instance or reuses an existing one, and hands it over:
Component
│
│ "I need UserService"
↓
Angular DI System
│
↓
UserService
The two words in the name describe exactly this. A dependency is anything a class needs to do its work. Injection means the framework delivers that dependency to the class instead of the class fetching or building it.
A first injection, end to end
Start with a small service. The @Injectable decorator marks the class as something Angular's DI can create, and providedIn: 'root' registers it application-wide:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserService {
getUserName() {
return 'Rahul';
}
}
The component asks for the service with inject(), imported from @angular/core alongside Component:
import { Component, inject } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
userService = inject(UserService);
}
When Angular constructs App, it evaluates this expression:
inject(UserService)
Angular reads it as "this class needs a UserService", resolves the dependency and assigns the result to the field. Because the field is public, the template can call the service directly:
<h1>{{ userService.getUserName() }}</h1>
Notice what is missing from the component. There is no construction call anywhere:
new UserService()
Creating and handing over the instance is entirely Angular's job.
Working with inject()
inject() is the modern API for requesting a dependency from Angular's DI system. You pass it a token, most often the service class itself, and receive the instance:
userService = inject(UserService);
The function comes from Angular's core package:
import { Component, inject } from '@angular/core';
With the field assigned in the class body:
userService = inject(UserService);
every method in the class can reach the service through this:
this.userService
For example, a method that reads the name and logs it:
loadUser() {
const name = this.userService.getUserName();
console.log(name);
}
One rule to remember: inject() works only inside an injection context. In practice that means field initializers, the constructor, and a few framework-managed places such as factory functions and functional guards. Calling it later, for example inside a click handler or a setTimeout callback, throws an error. Grab the dependency in a field, then use the field wherever you need it.
inject() compared with constructor injection
Older tutorials and many existing codebases request dependencies through constructor parameters:
constructor(private userService: UserService) {}
This constructor-based style is still fully supported. The field-based equivalent looks like this:
userService = inject(UserService);
Both forms talk to the same DI system and produce the same result. For new code, inject() tends to be shorter, fits naturally with standalone components and other modern Angular APIs, and avoids long constructor parameter lists, which becomes noticeable when you use inheritance. A typical class ends up looking like this:
export class App {
userService = inject(UserService);
}
Syntax is the least important part, though. What matters is the principle: the component receives its dependency from Angular rather than creating it.
Providers: telling Angular how to supply a dependency
Requesting a dependency only works if Angular knows how to produce it. That knowledge lives in a provider. The most common way to declare one is in the @Injectable decorator:
@Injectable({
providedIn: 'root'
})
export class UserService {
}
The key option is this single line:
providedIn: 'root'
It registers UserService with the application's root injector. From then on, any request like the following can be satisfied:
inject(UserService)
A helpful way to picture a provider is as a recipe that Angular keeps on file:
"When someone asks for UserService,
here is how you should provide it."
Registering at the root with providedIn
You may already have seen this configuration when creating services:
@Injectable({
providedIn: 'root'
})
With providedIn: 'root', the service belongs to the root-level injector. Angular creates one instance the first time something asks for it and reuses that same instance for every later request, which makes it an application-wide singleton. The full service looks like this:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserService {
getUserName() {
return 'Rahul';
}
}
Any component can then request it without listing the service in a providers array:
userService = inject(UserService);
For most application-wide services this is the simplest and preferred approach. It has a useful side effect as well: if nothing ever injects the service, build tools can drop it from the bundle, because the registration lives on the service rather than in a module or component.
Registering a service on a component
A provider can also be attached to a specific component through its providers array:
@Component({
selector: 'app-user',
providers: [UserService],
templateUrl: './user.html'
})
export class User {
}
In this case UserService is provided by that component's own injector rather than only by the root:
providedIn: 'root'
The difference between these two registrations is the starting point for understanding hierarchical injectors.
Hierarchical injectors
Angular does not use one global container. Injectors form a tree that roughly mirrors the component tree, with the root injector at the top:
Root Injector
│
├── App
│
├── Component A
│
│ └── Child Component
│
└── Component B
A service provided at the root is available almost everywhere. A component, however, can register its own provider for the same service:
@Component({
selector: 'app-user',
providers: [UserService],
templateUrl: './user.html'
})
export class User {
}
Now Angular maintains a provider tied to that component's injector. When the component or any of its descendants requests UserService, Angular uses the instance from that level. Each instance of the component gets its own copy of the service, created with the component and destroyed with it. Components elsewhere in the tree continue to see the root instance.
Why multiple levels are useful
Consider a SettingsService. Most screens should share a single set of settings, so providing it at the root makes sense:
Root
│
├── Home
├── Products
└── Profile
Now suppose the admin area needs settings of its own that should not leak into the rest of the app:
Root
│
├── Home
│
└── Admin
│
└── Admin Settings Service
By providing the service on the admin component, that subtree receives a separate instance while everything else keeps using the shared one. Per-feature state, per-dialog state and per-list-item state are all common uses of this pattern, and none of them is possible when every component simply calls new.
How Angular resolves a request
When Angular encounters a request such as:
userService = inject(UserService);
it searches for a provider starting at the injector closest to the requester and moving outward:
Component requests UserService
↓
Check current injector
↓
Provider found?
↙ ↘
Yes No
↓ ↓
Provide it Look at parent
↓
Continue upward
The first injector that has a matching provider wins. If the search reaches the top without finding one, Angular throws a DI error, typically reported as NullInjectorError: No provider for UserService. That is why every injectable service must be registered somewhere the requester can reach.
Injecting several dependencies
A component can depend on as many services as it needs. Here a component requests both a user service and a notification service:
import { Component, inject } from '@angular/core';
import { UserService } from './user.service';
import { NotificationService } from './notification.service';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
userService = inject(UserService);
notificationService = inject(NotificationService);
}
A method can then combine them, for example to greet the current user:
showWelcomeMessage() {
const name = this.userService.getUserName();
this.notificationService.show(
`Welcome ${name}`
);
}
Neither service is created by the component; Angular constructs and injects both.
Services and DI are related but distinct
It is easy to use the two terms interchangeably, but they describe different things. A service is a class that holds reusable logic or shared state. Dependency injection is the mechanism that delivers that service to whichever parts of the application need it:
Service
↓
Provides functionality
Dependency Injection
↓
Provides the service where needed
Component
↓
Uses the service
Applied to a concrete service:
UserService
↓
Angular DI
↓
User Component
↓
Uses UserService
Keeping the roles separate is what lets you change how or where a service is provided without touching the components that consume it.
Combining DI with signals
DI pairs naturally with signals for shared reactive state. The service below owns a counter signal and a method that updates it:
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CounterService {
count = signal(0);
increase() {
this.count.update(value => value + 1);
}
}
A component injects the service like any other:
import { Component, inject } from '@angular/core';
import { CounterService } from './counter.service';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
counterService = inject(CounterService);
}
Its template reads the signal by calling it, and the button calls the service method:
<h1>Count: {{ counterService.count() }}</h1>
<button (click)="counterService.increase()">
Increase
</button>
Three concepts cooperate here:
Dependency Injection
↓
CounterService
↓
Signal
↓
Component
↓
Template
The service owns the state, DI makes the same service instance available to every consumer, and the signal ensures the template updates when the value changes. Because CounterService is provided at the root, every component that injects it sees the same count. Providing it on a component instead would give each subtree its own independent counter.
What DI buys you
The benefits compound as an application grows:
- Organization: components do not need to know how their dependencies are built or what those dependencies need.
- Reuse: one service can serve many components.
- Testability: in tests you can supply a fake or stubbed provider in place of the real service, for example one that returns fixed data instead of calling an API.
- Flexibility: the injector hierarchy lets you choose between one shared instance and scoped instances.
- Loose coupling: a component depends on what a service offers, not on how to construct it.
A shopping app as an example
Picture an online store with a handful of services, each with one responsibility:
ProductService
↓
Gets products from API
↓
CartService
↓
Manages cart state
AuthService
↓
Manages authentication
NotificationService
↓
Displays notifications
Individual components then inject only what they use; a navigation bar, for instance, might need both authentication and cart state:
ProductComponent
↓
ProductService
↓
CartComponent
↓
CartService
Navbar
↓
AuthService
↓
CartService
Angular wires all of this together, and each component stays focused on its own UI rather than on creating and managing the objects behind it.
The most common mistake: a service with no provider
Beginners often write a plain class:
export class UserService {
}
and then try to inject it:
userService = inject(UserService);
With no provider registered anywhere, Angular has no recipe for the class and the injection fails with the missing-provider error described earlier. For an application-wide service, the fix is to add the decorator with a root registration:
@Injectable({
providedIn: 'root'
})
export class UserService {
}
Now the root injector knows how to create the service. Alternatively, list the class in a component's providers array if you want a scoped instance.
Recap
In modern Angular, a class obtains a dependency with inject():
userService = inject(UserService);
and a service makes itself available application-wide with:
@Injectable({
providedIn: 'root'
})
The providedIn setting decides where the service is registered, and the hierarchical injector system lets you share one instance across the whole app or scope separate instances to a component and its descendants. The full request flow looks like this:
Component
↓
Requests Dependency
↓
Angular DI
↓
Provider
↓
Service
And with signals added for reactive state:
Component
↓
inject()
↓
Service
↓
Signal
↓
Reactive UI
Key takeaways
- Components ask for dependencies; they do not create them. Angular finds a provider and supplies the instance.
- Call
inject()only in an injection context, such as a field initializer or constructor. providedIn: 'root'gives you a lazily created, tree-shakable singleton and is the right default for app-wide services.- A component-level
providersentry creates a separate instance for that component and its subtree. - Resolution walks up the injector tree; a missing provider anywhere on that path means a
NullInjectorError. - DI plus signals is a simple and effective pattern for shared state, and swapping providers is how you isolate components in tests.