Lesson 13 of 30
Angular doesn't hand you a service — you ask for one, and an injector tree figures out which instance you actually get. Understanding that tree explains almost every 'why do I have two instances of this?' bug.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Dependency injection means a class declares what it needs — "give me an HttpClient, give me an AuthService" — instead of constructing those things itself. Something else, the injector, is responsible for building or finding the right instance and handing it over. The benefit isn't abstract: it's that a component never has to know how to construct its dependencies, which means you can swap a real service for a fake one in a test, or a shared instance for a scoped one, without touching the component's own code.
The part that actually takes getting used to is that there isn't one injector — there's a tree of them, shaped like the component tree itself. Every component gets its own injector node (most of them empty, just forwarding upward). When a class asks for a dependency, Angular starts at that class's own injector and walks up the tree — parent, grandparent, all the way to the root injector — stopping at the first injector that has a provider registered for what's being asked for. Whichever injector answers first is the one whose instance you get.
Most services are registered once, at the root, and never need to be scoped any lower — @Injectable({ providedIn: 'root' }) puts a provider directly on the root injector, so every request for that service anywhere in the app resolves to the exact same singleton instance. But a component can also register its own provider — providers: [SomeService] in @Component metadata — which creates a new injector node with its own instance, shadowing anything above it for that component and its descendants only.
import { Injectable, Component, inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class AnalyticsService {
private sessionId = crypto.randomUUID();
log(event: string) {
console.log(`[${this.sessionId}] ${event}`);
}
}
@Injectable() // no providedIn — must be explicitly provided somewhere
export class WizardStepService {
currentStep = 1;
next() { this.currentStep++; }
}
@Component({
selector: 'app-signup-wizard',
standalone: true,
providers: [WizardStepService], // new instance, scoped to this component subtree
template: `<app-wizard-step></app-wizard-step>`,
})
export class SignupWizardComponent {
private analytics = inject(AnalyticsService); // resolves to the one root instance
}
Every SignupWizardComponent on the page — say you render two side by side for an A/B test — gets its own WizardStepService instance, because the provider lives on the component's own injector node, and every descendant of that component resolves WizardStepService to that same scoped instance by walking up and stopping there. Meanwhile AnalyticsService, provided at root, resolves to the exact same singleton no matter which component asks for it, anywhere in the app — the walk up the tree never finds a closer provider, so it keeps going until it reaches root.
A component asks Angular's DI system for a service — say, UserService.
providedIn: 'root' is tree-shakable — a component-level providers array is not. A root-scoped service that's never actually injected anywhere gets dropped from the production bundle entirely; a service listed in a component's providers array ships regardless of whether that component is ever used, because Angular can't statically prove it's unreachable the same way.Walking up an injector tree until something answers the request is structurally the same shape as React's context lookup walking up the component tree until it finds a matching Provider — both let you register something once at an ancestor and have every descendant resolve to it, and both let a closer ancestor shadow a farther one for just its own subtree.