Explain Hierarchical Dependency Injection in Angular With a Real Bug Example

LowEasyAngular
Preparing for interviews?

Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.

Quick Answer

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.

Answer

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

providedIn: 'root'

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 providers

1 per component instance

Local, isolated state

Where you provide a service determines its lifetime and scope

The bug in code

You think you have one cache service, but you accidentally created many.

TYPESCRIPT
@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

How the injector tree affects instance sharing

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.

TYPESCRIPT
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)

providedIn: 'root'

Single instance for entire app

Feature-isolated state

Route/module providers

Scoped lifetime

Per-component isolated state

Component providers

New instance per component

Correct scoping strategy

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.

Similar questions
Guides
15 / 37