Skip to content
Angular ng services 4 min read

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:

  1. Angular can read the constructor and inject() calls of the class to resolve its own dependencies.
  2. With the providedIn option, the class registers itself with an injector, so anything in scope can ask for it without an explicit providers array.

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 providers array 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 valueScopeUse it for
'root'Application-wide singleton in the root injectorAlmost all stateless or app-global services
'platform'Shared across multiple Angular apps on the same pageRare; micro-frontend / multi-app pages
'any'A new instance per lazy-loaded injectorPer-lazy-module isolation
A specific EnvironmentInjectorThat environment’s scopeAdvanced, 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’s providers. 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 @Injectable to any class that uses inject() or has constructor dependencies, even if it is also listed in a providers array.
  • Prefer providedIn over NgModule/app providers arrays so unused services are eliminated from the bundle.
  • Reach for component-level providers (no providedIn) only when you genuinely need a fresh, scoped instance per component subtree.
  • Never register the same service in both providedIn: 'root' and a component’s providers unless you deliberately want two instances.
  • Keep services stateless and singleton where possible; isolate mutable, per-view state behind component-scoped providers.
Last updated June 14, 2026
Was this helpful?