Angular is a framework that strongly embraces the concept of dependency injection (DI) and services. Services in Angular are used to share logic, data, and state across multiple parts of an application. One of the most fundamental concepts when working with Angular services is understanding singleton services and provider scopes.
This post explores these concepts in detail, discussing how Angular manages service instances, how providers work at different levels, and how you can control their scope using decorators and configuration.
What Are Services in Angular?
A service in Angular is a class that holds reusable logic, data operations, or communication with external systems such as APIs. Services are usually independent of UI components and help keep the application modular and maintainable.
Example of a Simple Angular Service
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ApiService {
getData() {
return 'Hello from ApiService!';
}
}
This simple service can now be injected into any component or another service using Angular’s dependency injection system.
Understanding Dependency Injection in Angular
Dependency Injection (DI) is the mechanism that Angular uses to create and share service instances. Instead of manually creating service objects with new
, Angular automatically provides instances where needed.
Example: Injecting a Service into a Component
import { Component, OnInit } from '@angular/core';
import { ApiService } from './api.service';
@Component({
selector: 'app-root',
template: <h1>{{ message }}</h1>
})
export class AppComponent implements OnInit {
message = '';
constructor(private apiService: ApiService) {}
ngOnInit() {
this.message = this.apiService.getData();
}
}
Here, Angular injects a single instance of ApiService
into the component. But how does Angular know whether to create a new instance or use an existing one? That’s where provider scopes come in.
Understanding Provider Scopes
Providers define where and how a service is created and shared. In Angular, providers can exist at different levels in the dependency injection hierarchy. The scope of a provider determines the lifespan and visibility of a service instance.
Types of Provider Scopes
- Root Scope — Global singleton instance shared across the entire app.
- Module Scope — Service instance unique to a specific feature module.
- Component Scope — Service instance unique to a specific component and its children.
Singleton Services
A singleton service means that Angular creates only one instance of that service throughout the entire application. All components and modules share that same instance.
Singletons are ideal for:
- Shared application state
- Global configuration
- Caching data
- Centralized API calls
- Authentication state
Creating a Singleton Service Using providedIn: ‘root’
Angular provides a simple and modern way to register singleton services using the @Injectable
decorator with the providedIn
property.
Example: Singleton Service
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ApiService {
counter = 0;
increment() {
this.counter++;
}
}
In this example:
providedIn: 'root'
tells Angular to register this service at the root level.- Only one instance of
ApiService
will be created for the entire application.
Using the Singleton Service in Multiple Components
Let’s see how this singleton behaves when injected into multiple components.
Component 1
import { Component } from '@angular/core';
import { ApiService } from './api.service';
@Component({
selector: 'app-component-one',
template: <p>Component One Count: {{ apiService.counter }}</p>
})
export class ComponentOne {
constructor(public apiService: ApiService) {
this.apiService.increment();
}
}
Component 2
import { Component } from '@angular/core';
import { ApiService } from './api.service';
@Component({
selector: 'app-component-two',
template: <p>Component Two Count: {{ apiService.counter }}</p>
})
export class ComponentTwo {
constructor(public apiService: ApiService) {
this.apiService.increment();
}
}
If both components are rendered simultaneously, the counter value will be shared. Each component will increment the same counter because they both use the same instance.
Output Example
Component One Count: 2
Component Two Count: 2
This confirms that the same service instance is being shared — a true singleton.
When Singleton Services Are Useful
Singletons are particularly useful when you want to:
- Maintain global state (e.g., user authentication).
- Store cached API data to avoid redundant requests.
- Share configuration or constants across modules.
- Track analytics or logs from different parts of the app.
- Manage a centralized store for state management.
When Not to Use Singleton Services
While singletons are powerful, they are not always appropriate. You might not want a singleton when:
- The service holds data that should not be shared between different components or modules.
- You need separate instances for each feature (like independent timers, local form data, or isolated logic).
For these cases, you can scope your service locally to a module or a component.
Module-Level Providers
If you want each module to have its own instance of a service, you can register the service in that module’s providers
array.
Example: Module-Level Service
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FeatureComponent } from './feature.component';
import { FeatureService } from './feature.service';
@NgModule({
declarations: [FeatureComponent],
imports: [CommonModule],
providers: [FeatureService]
})
export class FeatureModule {}
Here, FeatureService
is unique to FeatureModule
. If the same service is used in another module, it will have a separate instance.
Component-Level Providers
You can also register a service directly at the component level. Each component will then have its own independent instance of that service.
Example: Component-Level Service
import { Component } from '@angular/core';
import { LocalService } from './local.service';
@Component({
selector: 'app-local',
template: `
<button (click)="increment()">Increment</button>
<p>Count: {{ localService.count }}</p>
`,
providers: [LocalService]
})
export class LocalComponent {
constructor(public localService: LocalService) {}
increment() {
this.localService.count++;
}
}
The LocalService
import { Injectable } from '@angular/core';
@Injectable()
export class LocalService {
count = 0;
}
Each instance of LocalComponent
will get its own unique LocalService
instance. No sharing occurs between components.
Comparing Singleton and Local Services
Feature | Singleton (Root) | Local (Component) |
---|---|---|
Instance Count | One global instance | One instance per component |
Lifetime | Until app is destroyed | Until component is destroyed |
Use Case | Global shared data | Isolated component data |
Provided In | providedIn: 'root' | providers: [Service] |
Using providedIn in Different Scopes
Angular allows you to control where your service is provided using the providedIn
metadata.
Root-Level Singleton
@Injectable({ providedIn: 'root' })
Feature Module Scoped Service
@Injectable({ providedIn: 'any' })
This creates one instance per lazy-loaded module. It means each lazy-loaded module gets its own copy of the service, but eagerly loaded modules share the same instance.
Platform Scoped Service
@Injectable({ providedIn: 'platform' })
This is rarely used but can be helpful when multiple Angular applications share the same platform (e.g., micro-frontends).
The Hierarchical Injector System
Angular uses a hierarchical dependency injection system.
This means there isn’t just one injector, but multiple injectors arranged in a tree structure — similar to the component tree.
When a service is requested:
- Angular looks for the service in the current injector.
- If not found, it moves up to the parent injector.
- This continues until the service is found or the root injector is reached.
This hierarchical system is what allows services to have different scopes depending on where they are provided.
Example of Hierarchical Injection
@Component({
selector: 'app-parent',
template: `
<app-child></app-child>
`,
providers: [LocalService]
})
export class ParentComponent {
constructor(public localService: LocalService) {}
}
@Component({
selector: 'app-child',
template: Child Count: {{ localService.count }}
,
})
export class ChildComponent {
constructor(public localService: LocalService) {}
}
In this example:
- Both
ParentComponent
andChildComponent
share the same instance ofLocalService
because the provider is declared on the parent. - If the child also had its own provider, it would create a new instance.
Service Instance Behavior Summary
Provided Location | Instance Shared With | Lifetime |
---|---|---|
Root | Entire App | App lifetime |
Module | That Module | Module lifetime |
Component | That Component and Children | Component lifetime |
Lazy Loading and Singleton Behavior
Lazy-loaded modules in Angular have their own injectors.
If a service is provided in a lazy-loaded module, Angular creates a new instance for that module.
This can lead to separate instances of what might appear to be the same service.
Example
If a UserService
is provided in both the root module and a lazy-loaded feature module, two different instances will exist.
To avoid confusion, services meant to be globally shared should always use providedIn: ‘root’.
Practical Use Cases
Use Case 1: Global State Management
A singleton service can store user session data, theme settings, or application-level state.
@Injectable({ providedIn: 'root' })
export class AppStateService {
isDarkMode = false;
toggleTheme() {
this.isDarkMode = !this.isDarkMode;
}
}
Use Case 2: API Communication
A singleton API service can handle all HTTP calls and caching.
@Injectable({ providedIn: 'root' })
export class ApiService {
constructor(private http: HttpClient) {}
getUsers() {
return this.http.get('/api/users');
}
}
Use Case 3: Component-Specific Counters
A local service can manage data for a single component without affecting others.
@Injectable()
export class CounterService {
count = 0;
}
@Component({
selector: 'app-counter',
template: `
<button (click)="increment()">Increment</button>
<p>Value: {{ counterService.count }}</p>
`,
providers: [CounterService]
})
export class CounterComponent {
constructor(public counterService: CounterService) {}
increment() {
this.counterService.count++;
}
}
Each CounterComponent
instance has its own CounterService
, so they work independently.
Common Pitfalls and Mistakes
- Providing the same service in multiple places
- This creates multiple instances, often unintentionally.
- Solution: Use
providedIn: 'root'
for shared services.
- Forgetting to mark service as injectable
- Without
@Injectable()
, Angular cannot inject dependencies.
- Without
- Mixing component-level and root-level providers
- Causes unexpected behavior when state is not shared properly.
- Using providedIn: ‘any’ without understanding lazy-loading
- Leads to multiple instances when lazy-loaded modules exist.
Testing Singleton Services
When writing unit tests, it’s important to understand how service instances behave.
Example: Testing a Singleton Service
import { TestBed } from '@angular/core/testing';
import { ApiService } from './api.service';
describe('ApiService', () => {
let service: ApiService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ApiService);
});
it('should be a singleton', () => {
const another = TestBed.inject(ApiService);
expect(service).toBe(another);
});
});
This ensures that Angular provides the same instance every time it is injected.
Advanced: Custom Provider Tokens
You can define custom providers using tokens if you want multiple implementations of the same interface.
import { InjectionToken } from '@angular/core';
export const API_URL = new InjectionToken<string>('API_URL');
@NgModule({
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' }
]
})
export class CoreModule {}
Then inject it like this:
constructor(@Inject(API_URL) private apiUrl: string) {}
This approach works perfectly with singleton services when you need configurable dependencies.
Summary
- A singleton service is created once and shared across the entire app.
- Use
providedIn: 'root'
to make a service a global singleton. - To create isolated instances, provide the service at the component or module level.
- Angular’s hierarchical injectors determine which instance of a service is used.
- Lazy-loaded modules can have their own injector scopes.
- Always plan your provider scopes carefully to avoid unwanted duplication.
Final Example Summary
Singleton Service
@Injectable({ providedIn: 'root' })
export class ApiService {}
Component-Level Instance
@Component({
providers: [LocalService]
})
export class LocalComponent {}
Module-Level Instance
@NgModule({
providers: [FeatureService]
})
export class FeatureModule {}
Each of these represents a different provider scope in Angular’s dependency injection system.
Leave a Reply