1. What is Hierarchical Dependency Injection in Angular?
What is it?
Hierarchical Dependency Injection means Angular organizes its injectors in a parent-child hierarchy.
When Angular needs a dependency, it first looks in the nearest injector. If the dependency is not available there, Angular searches upward through parent injectors until it finds a matching provider.
Conceptually:
Root / Environment Injector
│
│ ProductService
│
▼
Parent Component Injector
│
▼
Child Component Injector
If the child requests ProductService:
Child Injector
↓ Not found
Parent Injector
↓ Not found
Root Injector
↓ Found
ProductService instance
↓
Child Component
This allows services to be either application-wide or limited to a particular component subtree.
2. Why do we need Hierarchical DI?
Not every service should necessarily be shared throughout the entire application.
For example:
Application
│
├── Header
├── Products
│ ├── ProductList
│ └── ProductDetails
│
└── Orders
You might want:
AuthService
↓
Entire Application
but another service might only be relevant to the Products area:
ProductStateService
↓
Products
├── ProductList
└── ProductDetails
Hierarchical DI allows Angular to control the scope and sharing of service instances.
3. Root-Level Service
A root-level service is commonly configured using:
@Injectable({
providedIn: 'root'
})
export class ProductService {
}
This makes the service available through the application's root environment injector.
Conceptually:
Root Injector
│
└── ProductService instance
↑
┌─────┼─────┐
│ │ │
A B C
Components throughout the application can normally receive the same root-level instance.
This is commonly used for:
- Authentication
- API services
- Logging
- Global state
- Configuration
- Caching
- User information
4. Component-Level Service
A service can instead be provided directly by a component:
@Component({
selector: 'app-product',
providers: [ProductService]
})
export class Product {
}
Now Angular creates an instance associated with that component's injector.
Root Injector
│
▼
Product Component
│
└── ProductService
│
├── Product Child A
│
└── Product Child B
That instance is available to the component and generally its descendants.
5. Full Simple Sample Program
This example clearly demonstrates the difference.
counter.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CounterService {
count = 0;
increment(): void {
this.count++;
}
}
component-a.ts
import { Component } from '@angular/core';
import { CounterService } from './counter.service';
@Component({
selector: 'app-component-a',
standalone: true,
template: `
<h2>Component A</h2>
<p>Count: {{ counterService.count }}</p>
<button (click)="increment()">
Increment
</button>
`
})
export class ComponentA {
constructor(public counterService: CounterService) {
}
increment(): void {
this.counterService.increment();
}
}
component-b.ts
import { Component } from '@angular/core';
import { CounterService } from './counter.service';
@Component({
selector: 'app-component-b',
standalone: true,
template: `
<h2>Component B</h2>
<p>Count: {{ counterService.count }}</p>
<button (click)="increment()">
Increment
</button>
`
})
export class ComponentB {
constructor(public counterService: CounterService) {
}
increment(): void {
this.counterService.increment();
}
}
app.ts
import { Component } from '@angular/core';
import { ComponentA } from './component-a';
import { ComponentB } from './component-b';
@Component({
selector: 'app-root',
standalone: true,
imports: [
ComponentA,
ComponentB
],
template: `
<h1>Hierarchical DI Example</h1>
<app-component-a></app-component-a>
<hr>
<app-component-b></app-component-b>
`
})
export class App {
}
6. How the Root-Level Example Works
The service contains:
@Injectable({
providedIn: 'root'
})
Therefore both components resolve CounterService from the root-level provider.
Root Injector
│
│ CounterService
│ count = 0
│
┌──┴──┐
│ │
▼ ▼
A B
Suppose Component A increments the value:
count = 1
Component B uses the same service instance, so it also observes:
count = 1
The state is shared.
7. Changing it to Component-Level
Now suppose we change ComponentA:
@Component({
selector: 'app-component-a',
standalone: true,
providers: [CounterService],
template: `
<h2>Component A</h2>
<p>Count: {{ counterService.count }}</p>
<button (click)="increment()">
Increment
</button>
`
})
export class ComponentA {
constructor(public counterService: CounterService) {
}
increment(): void {
this.counterService.increment();
}
}
The important line is:
providers: [CounterService]
Now Component A has a local provider.
8. What Happens Now?
There is already a root provider:
@Injectable({
providedIn: 'root'
})
But Component A also provides:
providers: [CounterService]
The hierarchy effectively becomes:
Root Injector
│
├── CounterService Instance #1
│
│
└──────────── Component B
Component A Injector
│
└── CounterService Instance #2
When Component A asks for:
CounterService
Angular searches the nearest injector first.
Component A Injector
↓
CounterService found
↓
Use local instance
It doesn't continue to the root injector.
Component B doesn't have a local provider:
Component B Injector
↓
Not found
↓
Root Injector
↓
CounterService found
↓
Use root instance
Therefore A and B now use different instances.
9. Angular's Injector Search Process
This is the central idea behind hierarchical DI.
Suppose:
Root
│
▼
Parent
│
▼
Child
The child requests:
ProductService
Angular effectively searches:
Child
↓
Is ProductService here?
NO
↓
Parent
↓
Is ProductService here?
NO
↓
Root
↓
Found
But if the parent provides it:
Root
│
│ ProductService #1
│
▼
Parent
│ ProductService #2
│
▼
Child
then the child receives:
ProductService #2
because the nearest matching provider wins.
10. Root-Level vs Component-Level Services
| Feature | Root-Level | Component-Level |
|---|---|---|
| Configuration | providedIn: 'root' |
providers: [Service] |
| Scope | Application-wide | Component subtree |
| Instance | Usually shared | Local instance |
| Available to | Entire application | Component + descendants |
| Lifetime | Usually application lifetime | Related to component instance |
| Shared state | Application-wide | Localized |
| Typical usage | Auth, API, logging | Local feature/component state |
11. Root-Level Example
@Injectable({
providedIn: 'root'
})
export class AuthService {
}
Architecture:
Root Injector
│
└── AuthService
│
┌──────┼──────┐
▼ ▼ ▼
Header Login Orders
This makes sense because authentication information is often required throughout the application.
12. Component-Level Example
Imagine a shopping-cart editor component that needs temporary local state:
@Component({
selector: 'app-cart-editor',
providers: [CartEditorService]
})
export class CartEditor {
}
If two instances of the component exist:
<app-cart-editor></app-cart-editor>
<app-cart-editor></app-cart-editor>
Angular can create separate service instances:
CartEditor #1
│
CartEditorService #1
CartEditor #2
│
CartEditorService #2
This is very useful when each component needs independent state.
13. Advantages of Hierarchical DI
Flexible Service Scope
You can decide whether a service should be:
Global
or
Local
Shared Global Services
Root-level services are useful when multiple unrelated components need the same functionality/state.
Isolated State
Component-level providers allow individual component instances to maintain separate state.
Automatic Dependency Resolution
Angular searches the injector hierarchy automatically.
Better Component Encapsulation
A component can own services that exist only for that component subtree.
14. Disadvantages / Things to Watch
Accidental Multiple Instances
This is the most important issue.
Suppose:
@Injectable({
providedIn: 'root'
})
export class UserService {}
but you also write:
@Component({
providers: [UserService]
})
The component can receive a different UserService instance.
If UserService contains shared state, this may cause unexpected behavior.
Injector Hierarchy Can Become Complex
In large applications:
Root
↓
Feature
↓
Parent
↓
Child
↓
Grandchild
different levels can provide the same token.
You need to understand which injector is actually providing the dependency.
15. Key Points
- Angular DI is hierarchical.
- Angular searches for dependencies starting from the nearest relevant injector and then moves upward.
- The nearest matching provider is normally used.
providedIn: 'root'is commonly used for application-wide services.- Root-level services are normally shared across the application.
providers: [Service]on a component creates a component-level provider.- Component-level services are available to that component and its descendants.
- Different component instances can get different service instances.
- Component-level providers are useful for isolated/local state.
- Be careful not to accidentally create multiple instances of a service that is supposed to hold global state.
16. Interview Questions and Answers
1. What is hierarchical Dependency Injection?
Hierarchical DI means Angular organizes injectors in a hierarchy. When resolving a dependency, Angular searches the nearest injector first and moves upward until it finds a matching provider.
2. What is a root-level service?
A root-level service is typically registered using:
@Injectable({
providedIn: 'root'
})
It is available throughout the application and normally uses a shared application-level instance.
3. What is a component-level service?
A component-level service is registered using:
providers: [ProductService]
inside @Component(). Its instance is scoped to that component and its descendant hierarchy.
4. What happens if the same service is provided at root and component level?
The component-level provider takes precedence for that component subtree.
Root → ProductService #1
Component
↓
ProductService #2
The component uses #2.
5. When should you use root-level services?
Use them when functionality or state needs to be broadly shared, such as:
- Authentication
- API access
- Logging
- Global state
- Configuration
6. When should you use component-level services?
Use them when the service/state should belong specifically to a component or component subtree, especially when each component instance needs independent state.
Interview Shortcut
ROOT LEVEL
@Injectable({
providedIn: 'root'
})
Application
↓
One broadly shared service instance
COMPONENT LEVEL
@Component({
providers: [MyService]
})
Component
↓
Local service instance
↓
Available to descendants
Most Important Rule
Angular searches from the nearest injector upward. If a service is provided locally at the component level, that local provider normally takes precedence over the root-level provider.