The @Injectable Decorator
The @Injectable decorator is how you tell Angular that a class can participate in dependency injection — both as something that receives dependencies and as something that can be provided to others. While any class can technically be injected, @Injectable is required when the class itself has dependencies, and its providedIn option is the modern, tree-shakable way to register a service. Getting this decorator right is the difference between a lean bundle with predictable singletons and a tangled web of duplicated instances.
What @Injectable does
@Injectable attaches dependency-injection metadata to a class. Concretely, it makes two things possible:
- Angular can read the constructor and
inject()calls of the class to resolve its own dependencies. - With the
providedInoption, the class registers itself with an injector, so anything in scope can ask for it without an explicitprovidersarray.
Without the decorator, a class that uses inject() or has constructor dependencies will throw at runtime because Angular has no metadata describing what to supply.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class TodoService {
private http = inject(HttpClient);
getTodos() {
return this.http.get<Todo[]>('/api/todos');
}
}
Tip: A class with no dependencies of its own that is only ever listed in a component’s
providersarray does not strictly need@Injectable. But always adding it is harmless, future-proofs the class against new dependencies, and keeps your codebase consistent.
The providedIn option
providedIn is the recommended way to register a service. Instead of adding the class to a providers array, you declare where it lives directly on the class. This keeps the registration co-located with the service and — crucially — makes the service tree-shakable: if nothing injects it, the bundler can drop it entirely from the build.
providedIn value | Scope | Use it for |
|---|---|---|
'root' | Application-wide singleton in the root injector | Almost all stateless or app-global services |
'platform' | Shared across multiple Angular apps on the same page | Rare; micro-frontend / multi-app pages |
'any' | A new instance per lazy-loaded injector | Per-lazy-module isolation |
A specific EnvironmentInjector | That environment’s scope | Advanced, scoped registration |
The overwhelmingly common choice is 'root':
@Injectable({ providedIn: 'root' })
export class AuthService {
private loggedIn = false;
login() { this.loggedIn = true; }
isLoggedIn() { return this.loggedIn; }
}
Because it is registered in the root injector, every component, directive, guard, and other service shares the same AuthService instance — a true application-wide singleton — without any of them listing it in a providers array.
Tree-shaking in practice
Tree-shaking is the headline benefit of providedIn. When a service registers itself this way, its registration only “counts” if some part of the application actually injects it. The bundler can statically prove an unused service is dead code and remove it.
Contrast that with the legacy pattern of registering in an NgModule.providers (or an app providers array) — there, the module references the service, so the bundler must keep it whether or not anything uses it.
// Tree-shakable: dropped from the bundle if never injected.
@Injectable({ providedIn: 'root' })
export class FeatureFlagsService { /* ... */ }
Output:
# Build report (illustrative)
FeatureFlagsService — referenced by 0 injectors → removed from bundle
AuthService — referenced by 4 injectors → included
Registering without providedIn
Sometimes you want a service scoped to a specific component subtree rather than the whole app — for example, a piece of state that should live and die with one feature. In that case omit providedIn and list the class in the component’s providers array. Each instance of that component then gets its own copy.
import { Component } from '@angular/core';
import { CartService } from './cart.service';
@Component({
selector: 'app-cart',
standalone: true,
providers: [CartService], // new CartService per <app-cart>
template: `<p>Items: {{ cart.count() }}</p>`,
})
export class CartComponent {
constructor(public cart: CartService) {}
}
Warning: A service can be registered in both
providedIn: 'root'and a component’sproviders. If you do this, the component-level provider wins for that subtree and you get a second instance — a frequent cause of “why is my state not shared?” bugs. Pick one strategy per service.
Functional alternatives
@Injectable decorates classes, but DI in modern Angular is not limited to classes. Functional guards, resolvers, and interceptors use inject() directly and need no decorator at all:
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isLoggedIn() || router.createUrlTree(['/login']);
};
Here AuthService still needs @Injectable({ providedIn: 'root' }), but the guard consuming it is a plain function.
Best Practices
- Default to
@Injectable({ providedIn: 'root' })for services — it is the simplest, most tree-shakable, and most common scope. - Always add
@Injectableto any class that usesinject()or has constructor dependencies, even if it is also listed in aprovidersarray. - Prefer
providedInoverNgModule/appprovidersarrays so unused services are eliminated from the bundle. - Reach for component-level
providers(noprovidedIn) only when you genuinely need a fresh, scoped instance per component subtree. - Never register the same service in both
providedIn: 'root'and a component’sprovidersunless you deliberately want two instances. - Keep services stateless and singleton where possible; isolate mutable, per-view state behind component-scoped providers.