Angular DI is hierarchical, so provider location changes instance count and service lifetime. The useful angle is the real production bug: re-providing a service in a component or standalone route and silently splitting shared state, caches, or coordination logic.
Use this Angular interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.
Explain Hierarchical Dependency Injection in Angular With a Real Bug ExampleFrontend interview answer
This Angular interview question tests whether you can explain Angular hierarchical DI in production: duplicate providers, shadowed singletons, and cache bugs, connect it to production trade-offs, and handle common follow-up questions.
- Angular hierarchical DI in production: duplicate providers, shadowed singletons, and cache bugs explanation without falling back to memorized docs wording
- Dependency Injection and Hierarchy reasoning, edge cases, and production failure modes
- How you would answer the most likely Angular interview follow-up
Bug-first mental model
Angular DI is not a single global container. It is a tree of injectors, and the nearest provider wins. That becomes a production bug when a root service is accidentally re-provided in a component or route and your “singleton” cache, auth flag, or shared state quietly splits into multiple instances.
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
}
];
Standalone route providers are their own injector boundary
In modern Angular, route-level providers create an EnvironmentInjector for that branch. That is why a service can be a singleton at the app root but still get a separate instance under one lazy route tree.
@Injectable({ providedIn: 'root' })
export class FeatureFlagsService {
enabled = false;
}
export const routes: Routes = [
{
path: 'reports',
providers: [FeatureFlagsService],
loadComponent: () => import('./reports.component').then(m => m.ReportsComponent)
}
];
// Outside /reports => root FeatureFlagsService
// Inside /reports => route-scoped FeatureFlagsService
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 or route providers
• Forgetting that standalone route providers create a new injector boundary
• 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. In standalone Angular, route providers are especially important because they create an EnvironmentInjector for that branch.
Use this as one explanation rep, then continue with the Angular interview questions cluster or a guided prep path.