Angular is one of the most powerful frameworks for building dynamic, single-page web applications. One of the main reasons Angular is so scalable and maintainable is its robust architecture centered around services and dependency injection (DI).
These two concepts — services and DI — enable Angular developers to write cleaner, modular, and testable code. They help you keep business logic separate from the view and allow code reuse across the application.
This article will guide you through everything you need to know about Angular services and dependency injection: what they are, how they work, and how to use them effectively in real-world applications.
Introduction to Angular Architecture
Before diving deep into services and dependency injection, it’s useful to recall how Angular applications are structured. Angular follows a component-based architecture. Each component controls a specific section of the UI, and together, they form a complete application.
However, as your app grows, certain logic needs to be shared between multiple components — for example:
- Fetching data from an API
- Validating user inputs
- Managing authentication
- Sharing user preferences
- Performing calculations or transformations
If each component contained its own copy of that logic, your code would become messy, inconsistent, and difficult to maintain.
This is where Angular services come in.
What Are Angular Services?
A service in Angular is a class that contains reusable logic or data that you want to share across multiple components, directives, or even other services.
Services are not tied to any particular view or component. Instead, they provide functionality such as data handling, state management, communication with APIs, and utilities.
By moving logic from components into services, you make your application modular, reusable, and testable.
Example: Without a Service
Let’s say you have two components: UserListComponent and UserDetailComponent. Both need to fetch user data from an API.
Without a service, each component might duplicate code like this:
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html'
})
export class UserListComponent implements OnInit {
users: any[] = [];
constructor(private http: HttpClient) {}
ngOnInit() {
this.http.get('https://api.example.com/users')
.subscribe(data => this.users = data as any[]);
}
}
Now imagine UserDetailComponent also does the same API call. That means duplicated logic, repeated URLs, and more maintenance overhead.
Example: With a Service
You can refactor this logic into a UserService and inject it wherever needed.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://api.example.com/users';
constructor(private http: HttpClient) {}
getUsers(): Observable<any[]> {
return this.http.get<any[]>(this.apiUrl);
}
getUser(id: number): Observable<any> {
return this.http.get<any>(${this.apiUrl}/${id});
}
}
Now, in your component:
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html'
})
export class UserListComponent implements OnInit {
users: any[] = [];
constructor(private userService: UserService) {}
ngOnInit() {
this.userService.getUsers().subscribe(data => this.users = data);
}
}
This is cleaner, easier to test, and follows the Single Responsibility Principle — components handle the view, services handle the logic.
Why Use Services?
Angular services offer several advantages that make applications more maintainable and scalable:
- Code Reusability
Logic can be reused across multiple components. - Separation of Concerns
Components focus on presentation; services handle data and logic. - Testability
Services can be tested independently of UI components. - Maintainability
Centralized logic is easier to update in one place. - Dependency Injection Support
Angular’s DI system automatically handles service creation and sharing.
What is Dependency Injection (DI)?
Dependency Injection (DI) is a design pattern where objects receive their dependencies instead of creating them directly.
In simple terms, if a class depends on another class (like a component depending on a service), Angular will automatically inject that dependency rather than you manually creating an instance.
This promotes loose coupling and improves testability.
Example: Without Dependency Injection
class UserService {
getUsers() {
return ['John', 'Jane', 'Alice'];
}
}
class UserComponent {
userService: UserService;
constructor() {
this.userService = new UserService(); // manually creating the dependency
}
showUsers() {
console.log(this.userService.getUsers());
}
}
Here, UserComponent is tightly coupled with UserService — it creates an instance directly.
If UserService changes, you may have to update multiple components. Testing becomes harder because you can’t easily replace the service with a mock.
Example: With Dependency Injection
Angular’s DI system handles this automatically:
@Injectable({
providedIn: 'root'
})
export class UserService {
getUsers() {
return ['John', 'Jane', 'Alice'];
}
}
@Component({
selector: 'app-user',
template: <p>{{ users }}</p>
})
export class UserComponent {
users: string[];
constructor(private userService: UserService) {
this.users = this.userService.getUsers();
}
}
Here, Angular’s injector creates and provides an instance of UserService for the component. The component never needs to manually construct it.
This makes your code loosely coupled and easily testable.
How Angular’s Dependency Injection Works
Angular’s DI system uses an injector — a container that knows how to create and supply instances of services and dependencies.
Whenever a component, directive, or service needs another dependency, Angular looks for it in the injector hierarchy.
If it finds it, it provides that instance. If not, it creates one based on the service’s provider configuration.
The Injector Hierarchy
Angular has a hierarchical injector system, which means different levels of injectors exist in your application:
- Root Injector – Created when the app starts. Services provided here are singletons (shared across the app).
- Module Injector – Created for feature modules if they declare providers.
- Component Injector – Created when a component has its own providers.
If Angular can’t find a dependency in a local injector, it travels up the hierarchy until it reaches the root injector.
Registering a Service with @Injectable
The simplest way to register a service is by using the @Injectable decorator with the providedIn property.
Example
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private isLoggedIn = false;
login() {
this.isLoggedIn = true;
}
logout() {
this.isLoggedIn = false;
}
isAuthenticated(): boolean {
return this.isLoggedIn;
}
}
The key part is providedIn: 'root'.
This tells Angular to register this service with the root injector, making it available globally as a singleton.
Providing Services in a Module
Before providedIn was introduced, services were registered in the providers array of a module.
Example
@NgModule({
providers: [AuthService]
})
export class AppModule {}
This method still works, but providedIn is the preferred modern approach because it enables tree-shaking — Angular can remove unused services from the production build.
Providing Services at the Component Level
You can also provide a service in a component’s metadata. In this case, each instance of that component gets a new instance of the service.
Example
@Component({
selector: 'app-logger',
template: <p>Logger Component</p>,
providers: [LoggerService]
})
export class LoggerComponent {
constructor(private logger: LoggerService) {
this.logger.log('LoggerComponent created');
}
}
Each time LoggerComponent is created, Angular provides a new instance of LoggerService.
This is not a singleton; it’s useful for cases where components need isolated service instances.
Singleton vs Non-Singleton Services
| Type | Description | Example Use Case |
|---|---|---|
| Singleton Service | One shared instance across the app | Authentication, configuration, global state |
| Non-Singleton Service | New instance per component or module | Component-specific logic, isolated data |
If your service should share data globally, provide it at the root level. If it should be isolated, provide it at the component or module level.
Injecting Services into Other Services
Services can also depend on other services — this is known as service injection.
Example
@Injectable({
providedIn: 'root'
})
export class LoggerService {
log(message: string) {
console.log('Logger:', message);
}
}
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private logger: LoggerService) {}
fetchData() {
this.logger.log('Fetching data...');
return ['Data1', 'Data2', 'Data3'];
}
}
Angular handles the dependency graph automatically, ensuring that each service receives its required dependencies.
Using Services for State Management
Angular services can be used to share and manage state across components.
Example: Shared State Service
@Injectable({
providedIn: 'root'
})
export class CounterService {
private count = 0;
increment() {
this.count++;
}
decrement() {
this.count--;
}
getCount() {
return this.count;
}
}
You can now inject this service into multiple components, and they’ll share the same counter state.
In Component A
@Component({
selector: 'app-a',
template: `
<button (click)="increase()">Increment</button>
<p>Count: {{ counter }}</p>
`
})
export class ComponentA {
counter = 0;
constructor(private counterService: CounterService) {}
increase() {
this.counterService.increment();
this.counter = this.counterService.getCount();
}
}
In Component B
@Component({
selector: 'app-b',
template: `
<button (click)="decrease()">Decrement</button>
<p>Count: {{ counter }}</p>
`
})
export class ComponentB {
counter = 0;
constructor(private counterService: CounterService) {}
decrease() {
this.counterService.decrement();
this.counter = this.counterService.getCount();
}
}
Both components now share the same CounterService instance. Incrementing or decrementing in one updates the other.
Using Observables in Services
For more dynamic state management, you can use RxJS observables in services.
Example: Reactive State Service
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class MessageService {
private messageSource = new BehaviorSubject<string>('Default Message');
currentMessage = this.messageSource.asObservable();
changeMessage(message: string) {
this.messageSource.next(message);
}
}
Now, any component can subscribe to currentMessage and reactively receive updates.
In Component A
@Component({
selector: 'app-a',
template: `
<input #msgInput (input)="update(msgInput.value)">
`
})
export class ComponentA {
constructor(private messageService: MessageService) {}
update(value: string) {
this.messageService.changeMessage(value);
}
}
In Component B
@Component({
selector: 'app-b',
template: `
<p>Message: {{ message }}</p>
`
})
export class ComponentB {
message: string = '';
constructor(private messageService: MessageService) {}
ngOnInit() {
this.messageService.currentMessage.subscribe(msg => this.message = msg);
}
}
Now, typing in Component A instantly updates Component B — a perfect demonstration of how dependency injection and services create reactive, connected components.
Dependency Injection Tokens
Sometimes, you might want to inject values that aren’t services — such as constants or configurations. Angular allows this through injection tokens.
Example
import { InjectionToken } from '@angular/core';
export const API_URL = new InjectionToken<string>('apiUrl');
Then, you can provide and inject it:
@NgModule({
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' }
]
})
export class AppModule {}
Now inject it into a service:
@Injectable({
providedIn: 'root'
})
export class ConfigService {
constructor(@Inject(API_URL) private apiUrl: string) {}
getUrl() {
return this.apiUrl;
}
}
This pattern is helpful for managing environment variables or global constants.
Testing Services and Dependency Injection
One of the biggest advantages of DI is that it simplifies testing.
You can easily mock dependencies when testing components or services.
Example: Mocking a Service in a Test
class MockUserService {
getUsers() {
return ['Mock User 1', 'Mock User 2'];
}
}
describe('UserComponent', () => {
let component: UserComponent;
let mockService: MockUserService;
beforeEach(() => {
mockService = new MockUserService();
component = new UserComponent(mockService as any);
});
it('should get mock users', () => {
expect(component.users).toEqual(['Mock User 1', 'Mock User 2']);
});
});
Common Mistakes to Avoid
- Providing Services in Multiple Modules
This can unintentionally create multiple instances instead of a singleton. - Forgetting
@Injectable()Decorator
Without it, Angular cannot inject dependencies. - Declaring Services in Component Providers
Unless you need isolated instances, provide them at the root. - Not Using Observables for Reactive Data
Services can become more powerful when combined with RxJS.
Best Practices for Angular Services and DI
- Use
providedIn: 'root'for global singletons. - Keep services focused on one responsibility.
- Avoid direct instantiation with
new. - Use interfaces for service contracts.
- Combine DI with RxJS for real-time updates.
- Make services stateless where possible — rely on streams for state.
- Use injection tokens for configuration values.
Real-World Example: API Service with Dependency Injection
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(private http: HttpClient) {}
get<T>(url: string): Observable<T> {
return this.http.get<T>(url);
}
post<T>(url: string, data: any): Observable<T> {
return this.http.post<T>(url, data);
}
}
@Injectable({
providedIn: 'root'
})
export class ProductService {
private baseUrl = 'https://api.example.com/products';
constructor(private api: ApiService) {}
getProducts() {
return this.api.get(this.baseUrl);
}
createProduct(product: any) {
return this.api.post(this.baseUrl, product);
}
}
The ProductService depends on ApiService, and Angular handles this dependency automatically. Both services remain modular and reusable.
Leave a Reply