In Angular, services play a crucial role in organizing and sharing data or logic across multiple components. One of the most powerful concepts in Angular service architecture is the singleton service. A singleton service is instantiated only once and shared across the entire application. This design pattern promotes efficient memory use, simplifies data sharing, and maintains consistent application state.
This comprehensive guide will walk you through everything you need to know about singleton services in Angular — from what they are and how they work to why and when you should use them, complete with code examples and best practices.
Introduction to Services in Angular
Before diving into singleton services, it’s important to understand what services are in Angular.
In Angular, a service is a class with a specific purpose. It typically contains logic that you want to share across multiple components. Services are commonly used for:
- Fetching data from an API
- Managing application state
- Logging and analytics
- Caching and configuration
- Authentication and user sessions
Instead of duplicating logic across components, you can move that logic into a service and inject it wherever needed.
What is a Singleton Service?
A singleton service is a service that has only one instance created throughout the lifetime of the application. All components and modules that inject this service share the same instance.
In other words, no matter how many components use it, the service’s data and state remain consistent across the app.
Angular creates and manages singleton services automatically when they are registered properly, ensuring efficient use of resources.
How Angular Creates Singleton Services
Angular uses its dependency injection (DI) system to create and manage service instances. When a service is provided at the root level using providedIn: 'root'
, Angular registers it with the root injector. As a result, Angular creates a single instance of that service and shares it across the entire application.
Example
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class LoggerService {
log(message: string) {
console.log(LoggerService: ${message}
);
}
}
Here, LoggerService
is a singleton because it is provided in the root injector. This means that any component or service that injects LoggerService
will receive the same instance.
Why Use Singleton Services?
Singleton services are ideal when you need to share data or state between multiple components or modules. Some common use cases include:
- Global State Management
Managing application-wide data such as user authentication status or theme preferences. - Caching
Storing fetched data locally to prevent unnecessary API calls. - API Communication
Providing a single point for handling all HTTP requests to a backend API. - Application Configuration
Sharing configuration values or environment-specific settings. - User Session Management
Tracking user activity or maintaining login state throughout the app.
Creating a Singleton Service Using providedIn: 'root'
The simplest and most modern way to create a singleton service in Angular is by using the providedIn
property in the @Injectable
decorator.
Example: Global State Service
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class GlobalStateService {
private count = 0;
increment() {
this.count++;
}
getCount() {
return this.count;
}
}
Now, when you inject GlobalStateService
into multiple components, they will all share the same data.
Using the Singleton Service in Components
import { Component } from '@angular/core';
import { GlobalStateService } from './global-state.service';
@Component({
selector: 'app-counter',
template: `
<h2>Counter: {{ count }}</h2>
<button (click)="increment()">Increment</button>
`
})
export class CounterComponent {
count = 0;
constructor(private globalState: GlobalStateService) {}
increment() {
this.globalState.increment();
this.count = this.globalState.getCount();
}
}
If you use this component in multiple places, all instances will share the same counter value because the service is a singleton.
How Angular Manages Service Instances
Angular uses an injector hierarchy. The root injector is created when the application starts, and additional injectors can exist at the module or component level.
Levels of Service Providers
- Root Injector (
providedIn: 'root'
)
A single instance is shared across the entire application. - Module-Level Provider (
providers
in@NgModule
)
Creates a new instance per module. - Component-Level Provider (
providers
in@Component
)
Creates a new instance for each component that declares it.
If you want a singleton, it should be provided at the root level.
Example: Singleton vs Non-Singleton Behavior
Singleton Example
@Injectable({
providedIn: 'root'
})
export class AppService {
data = 'Shared Data';
}
All components injecting AppService
will share the same data
property.
Non-Singleton Example
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
providers: [AppService] // Creates a new instance for this component
})
export class ChildComponent {
constructor(public appService: AppService) {}
}
In this case, every instance of ChildComponent
gets its own AppService
instance. It is not shared globally.
Using Singleton Services for API Calls
Singleton services are ideal for managing API communication because they provide a single place for handling HTTP requests, headers, and error handling.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ApiService {
private apiUrl = 'https://api.example.com';
constructor(private http: HttpClient) {}
getUsers(): Observable<any> {
return this.http.get(${this.apiUrl}/users
);
}
getUser(id: number): Observable<any> {
return this.http.get(${this.apiUrl}/users/${id}
);
}
}
Now, any component that injects ApiService
can make HTTP calls without re-creating new service instances.
Singleton Services for Caching
Caching data can improve performance and reduce redundant network calls. Singleton services make this simple since they retain state between component usages.
Example: Caching Data
@Injectable({
providedIn: 'root'
})
export class CacheService {
private cache: { [key: string]: any } = {};
set(key: string, value: any) {
this.cache[key] = value;
}
get(key: string): any {
return this.cache[key];
}
has(key: string): boolean {
return key in this.cache;
}
}
You can now inject CacheService
into any component and access the same cache data globally.
Singleton Services for Authentication
Authentication is another perfect example where singleton services shine.
@Injectable({
providedIn: 'root'
})
export class AuthService {
private loggedIn = false;
login() {
this.loggedIn = true;
}
logout() {
this.loggedIn = false;
}
isLoggedIn() {
return this.loggedIn;
}
}
This ensures consistent authentication state throughout the application.
Singleton Services and Lazy Loading
One common question developers face is:
Are singleton services still shared across lazy-loaded modules?
By default, Angular creates a separate injector for each lazy-loaded module. If a service is provided in the lazy-loaded module (not the root), it will not be a singleton across the app — it will be a singleton only within that module.
Example
If a service is provided in a lazy-loaded module:
@Injectable({
providedIn: 'any'
})
export class LazyService {}
Then, each lazy-loaded module using this service will get its own instance.
If you need a single instance across the entire app, always use:
@Injectable({
providedIn: 'root'
})
When Not to Use Singleton Services
While singleton services are powerful, they are not always appropriate. Avoid singletons when:
- You Need Isolated State Per Component
For example, in a dynamic component where each instance needs its own data. - Short-Lived Data
When the data is only relevant for a small part of the app and should not persist globally. - Memory Management
Singleton services persist for the app’s lifetime. Avoid using them for heavy objects or large data sets.
Debugging Singleton Behavior
To verify if your service is truly a singleton, you can use simple logging:
@Injectable({
providedIn: 'root'
})
export class DemoService {
constructor() {
console.log('DemoService instance created');
}
}
If the log appears only once during the app’s lifetime, it confirms the service is a singleton.
Best Practices for Singleton Services
- Use
providedIn: 'root'
The recommended approach for modern Angular applications. - Avoid Declaring in Multiple Modules
This can lead to multiple instances unintentionally. - Use Singleton for Global Logic Only
Keep component-specific logic inside the component. - Encapsulate Shared State
Use getter and setter methods instead of exposing public variables directly. - Use RxJS for Reactive State
Combine singleton services withBehaviorSubject
orReplaySubject
for real-time state updates.
Example: Singleton with Reactive State Management
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class StateService {
private messageSource = new BehaviorSubject<string>('Default Message');
currentMessage = this.messageSource.asObservable();
changeMessage(message: string) {
this.messageSource.next(message);
}
}
In Component A:
@Component({
selector: 'app-a',
template: `
<h3>Component A</h3>
<input #msgInput (input)="changeMessage(msgInput.value)">
`
})
export class ComponentA {
constructor(private stateService: StateService) {}
changeMessage(msg: string) {
this.stateService.changeMessage(msg);
}
}
In Component B:
@Component({
selector: 'app-b',
template: `
<h3>Component B</h3>
<p>Message: {{ message }}</p>
`
})
export class ComponentB {
message: string = '';
constructor(private stateService: StateService) {}
ngOnInit() {
this.stateService.currentMessage.subscribe(msg => this.message = msg);
}
}
Here, both components share a single instance of StateService
. When the message changes in one component, it instantly updates in the other.
Comparison: Singleton vs Non-Singleton Summary
Aspect | Singleton Service | Non-Singleton Service |
---|---|---|
Instance | Single shared instance | Separate instance per provider |
Lifetime | Entire application | Lifecycle of component or module |
Scope | Global | Localized |
Example Usage | Authentication, caching, API | Component-specific data |
Common Mistakes to Avoid
- Providing the Service in Multiple Modules
Leads to duplicate instances. - Declaring in Component Providers
Converts singleton to non-singleton unintentionally. - Not Using
providedIn
Always preferprovidedIn: 'root'
instead of module-level providers. - Misusing Singleton for Short-Lived State
Avoid persisting temporary data globally.
Complete Example: Singleton Service in Action
user.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserService {
private users: string[] = [];
addUser(user: string) {
this.users.push(user);
}
getUsers() {
return this.users;
}
}
app.component.ts
import { Component } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-root',
template: `
<h2>Add User</h2>
<input #userInput>
<button (click)="addUser(userInput.value)">Add</button>
`
})
export class AppComponent {
constructor(private userService: UserService) {}
addUser(user: string) {
this.userService.addUser(user);
}
}
list.component.ts
import { Component, OnInit } from '@angular/core';
import { UserService } from '../user.service';
@Component({
selector: 'app-list',
template: `
<h2>User List</h2>
<ul>
<li *ngFor="let user of users">{{ user }}</li>
</ul>
`
})
export class ListComponent implements OnInit {
users: string[] = [];
constructor(private userService: UserService) {}
ngOnInit() {
this.users = this.userService.getUsers();
}
}
In this example, both components share the same UserService
instance. When a user is added in AppComponent
, it instantly appears in ListComponent
because they share the same singleton service.
Leave a Reply