Skip to content

Angular Singleton Services

A singleton service has exactly one shared instance across the parts of the app that use it. This lesson explains how Angular creates singletons, and the subtle ways lazy loading can affect that guarantee.

What Makes a Service a Singleton?

A service becomes a singleton when it's registered once in an injector that every consumer shares, most commonly the root injector via providedIn: 'root'. Every component or service that injects it receives the exact same instance, making it ideal for shared application state.

Angular's providedIn: 'root' approach also produces tree-shakable providers: if nothing in your app ever injects the service, the bundler can remove it entirely from the production build.

@Injectable({ providedIn: 'root' })
export class CartService {
  private items = signal<string[]>([]);
  add(item: string) {
    this.items.update(list => [...list, item]);
  }
}

Every component that injects CartService shares the exact same items signal, since there's only one root-level instance.

Ways to Create a Singleton

@Injectable({ providedIn: 'root' })
export class MyService {}

// application-wide, at bootstrap
bootstrapApplication(AppComponent, {
  providers: [MyService],
});
  • providedIn: 'root' is the standard, tree-shakable way to create an app-wide singleton.
  • Listing a service in the bootstrap-level providers array also creates a single, app-wide instance.
  • Providing the same service again at a component or route level creates a separate instance for that subtree, breaking the singleton guarantee locally.
  • Avoid registering a providedIn: 'root' service again elsewhere unless you specifically want a scoped instance.

Singleton Services Cheatsheet

How different registration choices affect instance sharing.

Registration Result
@Injectable({ providedIn: 'root' }) One instance, shared app-wide, tree-shakable
Bootstrap providers: [MyService] One instance, shared app-wide, not tree-shakable
Component providers: [MyService] New instance per component instance
Route providers: [MyService] New instance per route activation
Lazy-loaded feature route with its own provider Separate instance scoped to that lazy chunk

Why providedIn: 'root' Is Tree-Shakable

Older Angular required listing every service in an NgModule's providers array, which meant the service was always bundled, even if unused. providedIn: 'root' embeds the provider information directly in the service's own metadata, so the bundler can detect when a service is never injected anywhere and remove it from the final bundle.

Lazy Loading and Singleton Scope

In modern Angular with the router's loadChildren/loadComponent, a providedIn: 'root' service is still a true application-wide singleton, lazy loading only affects how code is bundled and downloaded, not how root-level DI scoping works. Providing a service explicitly inside a lazy-loaded route's own providers array, however, creates a separate instance scoped to that route.

export const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes'),
    providers: [AdminOnlyService], // scoped only to the admin section
  },
];

When You Don't Want a Singleton

Some state genuinely should not be shared, form state for an editing session, or a wizard's step tracker for example. In those cases, register the service explicitly on the relevant component (or route) so each usage gets an isolated instance rather than sharing global state unintentionally.

@Component({
  selector: 'app-wizard',
  standalone: true,
  providers: [WizardStateService], // fresh instance per wizard usage
  template: '...',
})
export class WizardComponent {}

Common Mistakes

  • Assuming any injected service is automatically a singleton, scope always depends on where it's provided.
  • Registering a providedIn: 'root' service again on a component "just to be safe", accidentally creating a second, disconnected instance.
  • Storing session-specific or per-instance state in a true application-wide singleton, causing state to leak between unrelated usages.
  • Not realizing that lazy-loaded feature providers create their own scoped instance, separate from the root singleton.

Key Takeaways

  • providedIn: 'root' is the standard way to create a tree-shakable, application-wide singleton service.
  • Providing the same service again at a component or route level creates a new, separate instance for that subtree.
  • Lazy loading affects bundling, not root-level DI scope, root singletons remain singletons across lazy-loaded routes.
  • Deliberately scope services to a component or route when you want isolated, non-shared state.

Pro Tip

If you need per-feature isolated state, provide the service explicitly in that feature's route providers (or component providers) rather than trying to manually reset a root singleton's state between usages.