Angular’s dependency injection is hierarchical: every injector can create its own instance of a service. If you provide the same service in different places (root, module, component, route), you can accidentally get multiple instances and break shared state, caching, or coordination logic.
Explain Hierarchical Dependency Injection in Angular With a Real Bug Example
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Core idea
Angular does not have a single global injector. It has a tree of injectors. When Angular resolves a dependency, it starts from the current component’s injector and walks up the tree until it finds a provider. This means where you provide a service changes how many instances exist.
The real-world bug
“Why does my shared state reset when I navigate?” or “Why does my cache disappear in some components but not others?” The usual answer: you accidentally created multiple instances of the same service.
Where you provide the service | How many instances exist | Typical use |
|---|---|---|
| 1 for the whole app | Global singleton (auth, config, global cache) |
In a lazy-loaded route or module | 1 per lazy-loaded subtree | Feature-scoped state |
In a component’s | 1 per component instance | Local, isolated state |
The bug in code
You think you have one cache service, but you accidentally created many.
@Injectable({ providedIn: 'root' })
export class UserCacheService {
users: string[] = [];
}
@Component({
selector: 'app-user-list',
template: `...`,
// ❌ BUG: this creates a NEW instance just for this component
providers: [UserCacheService]
})
export class UserListComponent {
constructor(public cache: UserCacheService) {}
}
What goes wrong
Other parts of the app inject the root instance of UserCacheService, but UserListComponent gets its own private copy. Now state, caches, and flags are silently out of sync.
How Angular resolves the dependency
1) Check the component’s injector
2) If not found, check parent component injectors
3) Then module/route injectors
4) Finally root injector
The first match wins.
Injection site | Which instance you get | Why |
|---|---|---|
Component with its own providers | Component-scoped instance | Local provider shadows parent/root |
Child of that component | Same component-scoped instance | Injector tree inheritance |
Some other component elsewhere | Root instance | Different injector branch |
Another common real bug: lazy-loaded modules
If you provide a service in a lazy-loaded route/module, you get a separate instance from the rest of the app. This often breaks auth state, caches, or WebSocket connections.
export const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin.component'),
providers: [AdminStateService] // feature-scoped instance
}
];
When component-level providers are actually correct
• Reusable UI widgets with isolated state
• Temporary, per-instance state machines
• Editable rows, dialogs, or dynamic components that must not share state
Goal | Where to provide | Why |
|---|---|---|
Global shared state (auth, config, cache) |
| Single instance for entire app |
Feature-isolated state | Route/module providers | Scoped lifetime |
Per-component isolated state | Component | New instance per component |
Senior-level pitfalls
• Accidentally re-providing a root service in a component
• Breaking caches or auth state via lazy-loaded module providers
• Assuming “service = singleton” without checking injector scope
• Debugging “state randomly resets” for hours
Interview summary
Angular DI is hierarchical: services are resolved by walking up the injector tree. Where you provide a service determines how many instances exist. Many real-world bugs come from accidentally creating multiple instances by providing a service in a component or lazy-loaded route when you expected a singleton.