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
- Using providedIn (Recommended)
Best for tree-shakable and singleton services.
@Injectable({ providedIn: 'root' })
export class ApiService {}
- Using providers in Module
Scope limited to that module.
@NgModule({
providers: [UserService]
})
export class UserModule {}
- 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.
Top comments (0)