Dependency Injection in Angular: A Complete Guide with Use Cases



This content originally appeared on DEV Community and was authored by Rohit Singh

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
}

With DI:

class UserComponent {
  constructor(private userService: UserService) {} // loosely coupled
}

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 {}

🔹 Different Ways to Provide Dependencies

  1. Using providedIn (Recommended)

Best for tree-shakable and singleton services.

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

Scope limited to that module.

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

  1. Using providers in Component

Each component instance gets its own service.

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

🔹 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);
  }
}

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 {}

Usage:

constructor(@Inject(API_URL) private apiUrl: string) {}

✅ 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 }]
});

✅ 4. Different Implementations with useClass, useValue, useFactory

useClass: Provide alternate implementation.

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

useValue: Provide static values.

providers: [{ provide: 'APP_VERSION', useValue: '1.0.0' }]

useFactory: Dynamically decide dependency.

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

✅ 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 {}

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


This content originally appeared on DEV Community and was authored by Rohit Singh