DEV Community

Cover image for Dependency Injection in Angular: A Complete Guide with Use Cases
ROHIT SINGH
ROHIT SINGH

Posted on

Dependency Injection in Angular: A Complete Guide with Use Cases

When building scalable applications in Angular, one of the core concepts you’ll use daily is Dependency Injection (DI). It simplifies how your components and services interact by removing the responsibility of creating and managing dependencies.

In this blog, we’ll explore:

What Dependency Injection is in Angular

How Angular’s DI system works

Different ways to provide dependencies

Real-world use cases of DI

🔹 What is Dependency Injection?

Dependency Injection (DI) is a design pattern in which a class (like a component or service) gets its dependencies from an external source rather than creating them itself.

Without DI:

class UserComponent {
  userService = new UserService(); // tightly coupled
}
Enter fullscreen mode Exit fullscreen mode

With DI:

class UserComponent {
  constructor(private userService: UserService) {} // loosely coupled
}
Enter fullscreen mode Exit fullscreen mode

Here, Angular’s injector creates the UserService instance and passes it into the component automatically. This makes code cleaner, testable, and scalable.

🔹 How Angular’s DI System Works

Angular has a hierarchical injector system, meaning dependencies can be provided at different levels:

Root injector (providedIn: 'root') – Singleton across the app.

Module level (providers in @NgModule) – Shared within a module.

Component level (providers in @Component) – New instance for each component tree.

Example:

@Injectable({
  providedIn: 'root'
})
export class AuthService {}

Enter fullscreen mode Exit fullscreen mode

🔹 Different Ways to Provide Dependencies

  1. Using providedIn (Recommended)

Best for tree-shakable and singleton services.

@Injectable({ providedIn: 'root' })
export class ApiService {}
Enter fullscreen mode Exit fullscreen mode
  1. Using providers in Module

Scope limited to that module.

@NgModule({
  providers: [UserService]
})
export class UserModule {}

Enter fullscreen mode Exit fullscreen mode
  1. Using providers in Component

Each component instance gets its own service.

@Component({
  selector: 'app-user',
  providers: [UserService]
})
export class UserComponent {}
Enter fullscreen mode Exit fullscreen mode

🔹 Use Cases of Dependency Injection in Angular
✅ 1. Sharing Data Across Components

Instead of using Input/Output for deep component trees, you can use a shared service with DI.

@Injectable({ providedIn: 'root' })
export class SharedDataService {
  private message = new BehaviorSubject<string>('Hello');
  message$ = this.message.asObservable();

  updateMessage(msg: string) {
    this.message.next(msg);
  }
}
Enter fullscreen mode Exit fullscreen mode

Injected in multiple components, this keeps data in sync across your app.

✅ 2. Configurable Services with InjectionToken

When you want to inject configuration values (like API URLs).

export const API_URL = new InjectionToken<string>('API_URL');

@NgModule({
  providers: [{ provide: API_URL, useValue: 'https://api.example.com' }]
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Usage:

constructor(@Inject(API_URL) private apiUrl: string) {}
Enter fullscreen mode Exit fullscreen mode

✅ 3. Mocking Dependencies in Testing

DI makes unit testing easier by allowing you to swap services with mocks.

class MockAuthService {
  isLoggedIn = true;
}

TestBed.configureTestingModule({
  providers: [{ provide: AuthService, useClass: MockAuthService }]
});

Enter fullscreen mode Exit fullscreen mode

✅ 4. Different Implementations with useClass, useValue, useFactory

useClass: Provide alternate implementation.

providers: [{ provide: LoggerService, useClass: ConsoleLoggerService }]

Enter fullscreen mode Exit fullscreen mode

useValue: Provide static values.

providers: [{ provide: 'APP_VERSION', useValue: '1.0.0' }]
Enter fullscreen mode Exit fullscreen mode

useFactory: Dynamically decide dependency.

providers: [{
  provide: ApiService,
  useFactory: (env: EnvironmentService) => 
    env.isProd ? new ProdApiService() : new DevApiService(),
  deps: [EnvironmentService]
}]
Enter fullscreen mode Exit fullscreen mode

✅ 5. Feature-Specific Services at Component Level

If each component needs a separate instance of a service (like form state).

@Component({
  selector: 'app-register',
  providers: [FormStateService]
})
export class RegisterComponent {}
Enter fullscreen mode Exit fullscreen mode

Each RegisterComponent gets its own FormStateService instance.

🔹 Advantages of DI in Angular

✅ Promotes loose coupling

✅ Makes testing easier

✅ Reusable services

✅ Flexible configurations with tokens & factories

✅ Improves scalability for large applications

🔹 Final Thoughts

Dependency Injection is at the core of Angular’s architecture. By mastering it, you’ll write code that is modular, testable, and maintainable. Whether you’re sharing data across components, configuring environment-based services, or swapping out dependencies in tests — DI makes it seamless.

👉 If you’re building large Angular apps, think carefully about where to provide services (root, module, or component) and leverage tokens/factories for flexibility.

⚡ Pro Tip: Always prefer providedIn: 'root' for services unless you have a clear reason to scope them differently.

🚀 Rohit Singh 🚀 – Medium

Read writing from 🚀 Rohit Singh 🚀 on Medium. Full-stack developer with 6+ years in Angular, Node.js & AWS. Sharing tips, best practices & real-world lessons from building scalable apps.

favicon medium.com

Top comments (0)