Skip to content
Angular ng services 4 min read

Singleton Services

A singleton service is a class that Angular instantiates exactly once and then shares with every part of your application that asks for it. Singletons are the natural home for state and behaviour that must be consistent everywhere — the current user, a shopping cart, a feature-flag cache, or a wrapper around HttpClient. The modern, tree-shakable way to create one is providedIn: 'root', and getting it right means understanding which injector holds your instance.

What providedIn: 'root' actually does

When you decorate a service with @Injectable({ providedIn: 'root' }), you register a provider on the root environment injector — the single injector created when your app bootstraps. The first time any component, directive, guard, or other service injects the token, Angular creates one instance and caches it on that injector. Every subsequent request returns the same instance.

Because the provider lives on the root injector and the injector lives for the entire lifetime of the application, the service is effectively a global singleton — but a tree-shakable one. If nothing in your bundle ever injects the service, the optimizer drops it from the output entirely. This is why providedIn: 'root' is preferred over the older pattern of listing services in a module’s providers array.

import { Injectable, signal, computed } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class CartService {
  private readonly items = signal<string[]>([]);

  readonly count = computed(() => this.items().length);

  add(sku: string): void {
    this.items.update((list) => [...list, sku]);
  }

  clear(): void {
    this.items.set([]);
  }
}

Sharing one instance across components

Any standalone component can inject the service with inject() and they will all observe the same state. The header and the product page below read and mutate the same CartService.

import { Component, inject } from '@angular/core';
import { CartService } from './cart.service';

@Component({
  selector: 'app-header',
  standalone: true,
  template: `<span>Cart: {{ cart.count() }}</span>`,
})
export class HeaderComponent {
  protected readonly cart = inject(CartService);
}

@Component({
  selector: 'app-product',
  standalone: true,
  template: `<button (click)="cart.add('SKU-42')">Add to cart</button>`,
})
export class ProductComponent {
  protected readonly cart = inject(CartService);
}

Click the button once and the header immediately shows Cart: 1 because the signal lives on the one shared instance. No inputs, outputs, or event bus required.

Output:

Cart: 0
(user clicks "Add to cart")
Cart: 1

Proving it is a single instance

A quick way to confirm a service is a true singleton is to log a unique id from its constructor and inject it in two places.

import { Injectable, inject, Component } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class IdService {
  readonly id = Math.random().toString(36).slice(2, 8);
  constructor() {
    console.log('IdService created:', this.id);
  }
}

@Component({ selector: 'app-a', standalone: true, template: '' })
export class AComponent {
  constructor() {
    console.log('A sees', inject(IdService).id);
  }
}

Output:

IdService created: k3p9za
A sees k3p9za
B sees k3p9za

The constructor runs once; both components see the identical id.

Common pitfalls

Singletons are simple until they aren’t. The table below covers the mistakes that bite teams most often.

PitfallWhat goes wrongFix
Listing the service in a component’s providersA new instance is created per component, silently breaking shared stateRely on providedIn: 'root' only
Importing the service from two different file pathsTwo distinct classes, two singletonsUse one canonical import path / barrel
Lazy route also provides the serviceThe lazy-loaded child injector shadows the root oneDon’t re-provide root services in route providers
Mutable public fieldsAny consumer can corrupt shared stateExpose readonly signals + methods

Gotcha: Adding your “global” service to a route’s or component’s providers array does not make it more available — it creates a second, scoped instance that shadows the singleton. If a feature appears to “forget” data, this is almost always the cause.

Tip: Singletons live for the whole app, so they never get destroyed. Anything they subscribe to or setInterval they start will leak for the session — tear those down or prefer signals, which require no cleanup.

Singletons vs. scoped services

Reach for a singleton when the data is genuinely application-wide. When state belongs to one screen or widget, register the provider lower in the tree instead, so it is created and discarded with that part of the UI.

NeedUse
Auth/session, app config, caches, HTTP wrappersprovidedIn: 'root' singleton
State for one feature areaRoute-level providers
State isolated per component instanceComponent providers

Best Practices

  • Default to providedIn: 'root' for any service intended to be shared application-wide — it is tree-shakable and needs zero module wiring.
  • Never list a root singleton in a component or route providers array; doing so creates an unintended second instance.
  • Import each service from a single canonical path to avoid duplicate class identities and accidental duplicate singletons.
  • Expose state through readonly signals and methods rather than public mutable fields so consumers cannot corrupt shared data.
  • Avoid long-lived subscriptions and timers in singletons; prefer signals and computed so there is nothing to clean up.
  • Keep singleton APIs small and intentional — a global service is a global dependency, so resist turning it into a junk drawer.
Last updated June 14, 2026
Was this helpful?