Lesson 15 of 30
@Injectable({ providedIn: 'root' }) is the default for a reason — a tree-shakable singleton with no boilerplate registration. Knowing when to scope lower is the other half of the skill.
12 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
A service in Angular is just a class whose job is to hold logic or state that doesn't belong to any one component — an API client, a shopping cart, an auth session. The @Injectable() decorator marks a class as something the injector knows how to construct and hand out, and providedIn: 'root' is the option that answers the question every service needs answered: where does this get registered, and who shares the instance?
providedIn: 'root' means "register this on the app's root injector," which has two consequences worth understanding separately. First, it's a singleton — every part of the app that injects this service, no matter how deeply nested, resolves to the exact same instance, which is exactly what you want for something like an auth session or an API client where having two independent copies would be a bug, not a feature. Second, and less obviously, it's tree-shakable — because the provider is declared right on the @Injectable() decorator instead of listed manually in some central providers array, Angular's build tooling can see, for any given service, whether anything in the app actually injects it, and drop the whole class from the production bundle if not. That's the opposite of the old NgModule-era pattern, where every service got added to a module's providers array regardless of whether it ended up used — dead code that shipped anyway.
providedIn: 'root' is the right default for the overwhelming majority of services. The exception is when you deliberately want more than one instance to exist at once — and for that, you scope the service to a component instead.
import { Injectable, Component, inject, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ApiClient {
// one shared instance app-wide — exactly what you want for a fetch wrapper
async get<T>(path: string): Promise<T> {
const res = await fetch(`/api${path}`);
return res.json();
}
}
// no providedIn here — this class isn't usable until something provides it explicitly
@Injectable()
export class CartService {
private items = signal<{ id: string; qty: number }[]>([]);
readonly itemCount = this.items;
add(id: string) {
this.items.update((list) => [...list, { id, qty: 1 }]);
}
}
@Component({
selector: 'app-store-widget',
standalone: true,
providers: [CartService], // each <app-store-widget> gets its OWN cart
template: `<p>Items: {{ cart.itemCount().length }}</p>`,
})
export class StoreWidgetComponent {
protected cart = inject(CartService);
private api = inject(ApiClient); // resolves to the single root instance
}
If a page renders two <app-store-widget> instances side by side — say, comparing two different regional storefronts — each gets its own independent CartService, because the provider lives on the component, while both share the exact same ApiClient instance from root. That's the whole decision in one example: reach for providedIn: 'root' by default, and only drop to a component-level providers: [...] array when you specifically need isolated, per-instance state that shouldn't leak between multiple uses of the same component.
providedIn: 'root' and "provided in the root component's providers array" are not the same thing, even though they sound similar. The former registers on the actual root injector and is tree-shakable; manually listing a service in the root component's providers array creates a component-scoped provider that happens to be at the top of the tree, and it isn't tree-shakable the same way.CartService at the component level and back, and you get a brand-new instance with empty state — not the old one revived. If state needs to survive that, it belongs at root (or in a store), not on the component.providers: [CartService] on StoreWidgetComponent makes that instance available to the component itself and every descendant in its subtree — child components underneath it resolve to the same scoped instance, they don't each get their own.providedIn: 'root' giving you one shared instance app-wide is the same underlying idea as the module pattern hiding a single piece of state inside a closure that every caller shares — both are ways of saying "construct this once, and let everyone reach the same instance," just enforced by the framework's injector instead of by a function's own scope.