Implementing Dependency Inversion in Angular Using Injection Tokens: A Complete Guide

Implementing Dependency Inversion in Angular Using Injection Tokens: A Complete Guide

Introduction

Dependency Inversion Principle (DIP) is one of the five SOLID principles of software design that promotes loose coupling and enhances code maintainability. In Angular, DIP is implemented through Dependency Injection (DI), making the framework modular and scalable.

In this article, we'll explore what Dependency Inversion is, why it's important in Angular, and how to implement it using Injection Tokens. We'll also cover its pros and cons, ensuring a balanced perspective.


What is Dependency Inversion Principle (DIP)?

DIP states that high-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces or abstract classes).

Traditional Approach (Tightly Coupled Code)

class UserService {
  getUsers() {
    return ['Alice', 'Bob', 'Charlie'];
  }
}

class UserComponent {
  private userService: UserService;

  constructor() {
    this.userService = new UserService();
  }

  showUsers() {
    console.log(this.userService.getUsers());
  }
}

        

Problem: The UserComponent directly depends on UserService, making it difficult to replace UserService with a different implementation (e.g., fetching data from an API instead of a static list).


Implementing Dependency Inversion in Angular Using Injection Tokens

Step 1: Create an Interface (Abstraction)

export interface IUserService {
  getUsers(): string[];
}

        

Step 2: Define an Injection Token

import { InjectionToken } from '@angular/core';
import { IUserService } from './user-service.interface';

export const USER_SERVICE_TOKEN = new InjectionToken<IUserService>('USER_SERVICE_TOKEN');

        

Step 3: Implement the Interface

import { Injectable } from '@angular/core';
import { IUserService } from './user-service.interface';

@Injectable()
export class UserService implements IUserService {
  getUsers(): string[] {
    return ['Alice', 'Bob', 'Charlie'];
  }
}

        

Step 4: Provide the Dependency Using Injection Token (For NgModule-based Apps)

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { UserComponent } from './user.component';
import { UserService } from './user.service';
import { USER_SERVICE_TOKEN } from './user-service.token';
import { IUserService } from './user-service.interface';

@NgModule({
  declarations: [AppComponent, UserComponent],
  imports: [BrowserModule],
  providers: [{ provide: USER_SERVICE_TOKEN, useClass: UserService }],
  bootstrap: [AppComponent]
})
export class AppModule {}

        

Step 5: Inject the Dependency in a Component

import { Component, Inject } from '@angular/core';
import { USER_SERVICE_TOKEN } from './user-service.token';
import { IUserService } from './user-service.interface';

@Component({
  selector: 'app-user',
  template: `<div *ngFor="let user of users">{{ user }}</div>`
})
export class UserComponent {
  users: string[];

  constructor(@Inject(USER_SERVICE_TOKEN) private userService: IUserService) {
    this.users = this.userService.getUsers();
  }
}

        

Implementing Dependency Inversion in Standalone Components

With Angular's standalone components, the approach is slightly different as there is no NgModule. Instead, the provider is registered directly in the component or the application bootstrap.

Step 1: Provide Dependency in Standalone Component

import { Component, Inject, Injectable, InjectionToken } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

// Define an interface
export interface IUserService {
  getUsers(): string[];
}

// Create an Injection Token
export const USER_SERVICE_TOKEN = new InjectionToken<IUserService>('USER_SERVICE_TOKEN');

// Implement the service
@Injectable()
export class UserService implements IUserService {
  getUsers(): string[] {
    return ['Alice', 'Bob', 'Charlie'];
  }
}

// Define the Standalone Component
@Component({
  selector: 'app-user',
  standalone: true,
  template: `<div *ngFor="let user of users">{{ user }}</div>`,
  providers: [{ provide: USER_SERVICE_TOKEN, useClass: UserService }]
})
export class UserComponent {
  users: string[];

  constructor(@Inject(USER_SERVICE_TOKEN) private userService: IUserService) {
    this.users = this.userService.getUsers();
  }
}

// Bootstrap the application
bootstrapApplication(UserComponent);

        

This ensures that the service is provided at the component level, keeping the dependency injection localized.


Pros and Cons of Using Dependency Inversion with Injection Tokens in Angular

✅ Pros

  • Loose Coupling: Components are independent of concrete implementations.
  • Easier Testing: Mock services can be injected in unit tests.
  • More Flexible DI: Different implementations can be swapped easily using Injection Tokens.
  • Better Code Organization: Encourages modular design.

❌ Cons

  • More Boilerplate Code: Requires defining interfaces, tokens, and providers.
  • Slightly Increased Complexity: For small applications, it may not be necessary.
  • Performance Overhead: More indirections might slightly impact performance.


Conclusion

Implementing Dependency Inversion in Angular using Injection Tokens helps in writing scalable, maintainable, and testable applications. Whether using NgModule or Standalone Components, applying this principle ensures better modularity and flexibility.

By following the principles outlined in this article, you can build Angular applications that are extensible, loosely coupled, and future-proof. 🚀


💡 What’s Next?

Want to dive deeper? Explore:

  • Angular Dependency Injection Hierarchy
  • Unit Testing with Mock Injection Tokens
  • Using Multi-Providers with Injection Tokens

Follow me on:

Be sure to clap and follow the writer ️👏

Cheers to cleaner, better code!!!

And, if you found this article helpful, a clap and a follow would be much appreciated 😊😊

Happy Coding! 🎯

To view or add a comment, sign in

More articles by Rajat Malik

Insights from the community

Others also viewed

Explore topics