Swiftorial Logo
Home
Swift Lessons
Matchups
CodeSnaps
Tutorials
Career
Resources

Dependency Injection Overview in Angular

Angular’s Dependency Injection (DI) system provides a powerful way to manage and share services across your application. This guide covers the hierarchical nature of Angular’s injectors, how providers work at different levels, and best practices for structuring services to ensure modular, maintainable applications.

1. Introduction

Dependency Injection in Angular is a pattern where a class (such as a component or service) receives its dependencies from external sources rather than creating them itself. By default, Angular’s DI system is hierarchical, forming a tree of injectors that can provide different service instances based on the injector’s position in the hierarchy.

2. Hierarchical DI in Angular

At the root of every Angular application is the Root Injector, which typically provides services that are available application-wide. However, you can configure additional injectors at the module, component, or directive levels, allowing different parts of your application to have their own service instances.

  • Root Injector: Created during the application bootstrap. Services provided here are generally singletons and can be accessed throughout the application.
  • Module Injector: Each NgModule can declare its own providers. Services provided in feature modules can be scoped to that module (especially with lazy loading).
  • Component Injector: Specified via the @Component metadata’s providers array. Each component with a providers array creates a new injector for itself and its child components.

3. ProvidedIn Strategies

When creating a service, you can specify how and where it should be provided using the @Injectable() decorator’s providedIn option. Common values:

  • providedIn: 'root' – The service is provided in the root injector (singleton, app-wide).
  • providedIn: 'any' – A new instance is created in each lazy-loaded module. If the service is used in multiple modules, each gets its own instance.
  • providedIn: 'platform' – A special platform-level injector for multiple Angular apps running in the same document.
  • Omitting providedIn and instead specifying providers: [MyService] in a module or component is also valid, but less common with newer Angular best practices.

4. Example: Root vs. Component-Level Providers

Below is an example illustrating how a service provided at the root level differs from a service provided at the component level. Notice how each level creates its own instance of the service.

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { MyService } from './my-service.service';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [MyService], // Root-level provider (alternative to providedIn: 'root')
  bootstrap: [AppComponent]
})
export class AppModule { }

// my-service.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class MyService {
  private count = 0;

  increment() { this.count++; }
  getCount(): number { return this.count; }
}

// parent.component.ts
import { Component } from '@angular/core';
import { MyService } from './my-service.service';

@Component({
  selector: 'app-parent',
  template: '',
})
export class ParentComponent {
  constructor(private myService: MyService) {
    this.myService.increment();
  }
}

// child.component.ts
import { Component } from '@angular/core';
import { MyService } from './my-service.service';

@Component({
  selector: 'app-child',
  template: '

Count: {{ myService.getCount() }}

', providers: [MyService] // Component-level provider }) export class ChildComponent { constructor(public myService: MyService) { this.myService.increment(); } }

In this example:

  • The root-level MyService is a singleton shared across most of the app.
  • The MyService declared in ChildComponent is a different instance, isolated to that component’s injector.

5. Best Practices

  • Use ProvidedIn for Simplicity: Prefer providedIn: 'root' or providedIn: 'any' in services. It’s simpler than manually listing providers in modules.
  • Singleton Services: If you want a single shared instance, provide the service in the root injector (or an eagerly loaded module).
  • Component-Level Scoping: If a service is only needed by a specific component or subtree, declare it in that component’s providers. This reduces memory usage and prevents unintended sharing.
  • Lazy Loading: Use feature modules with providedIn: 'any' or module-level providers to create separate instances of a service for each lazy-loaded module.
  • Avoid Overusing Component Providers: While it can be useful for scoping, too many component-level services can complicate your architecture.

6. Further Examples: The forRoot() Pattern

Many libraries use a forRoot() static method on their main module to configure and provide services. This ensures a single instance of the service is created at the root level. Feature modules might have a forChild() method that doesn't provide the same services again, preventing duplicates. For example:

// my-feature.module.ts
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MyService } from './my-service.service';
import { MyFeatureComponent } from './my-feature.component';

@NgModule({
  imports: [CommonModule],
  declarations: [MyFeatureComponent],
  exports: [MyFeatureComponent]
})
export class MyFeatureModule {
  static forRoot(): ModuleWithProviders {
    return {
      ngModule: MyFeatureModule,
      providers: [MyService]
    };
  }
}

This pattern helps manage whether a service is provided at the root injector or just in a child module. It’s common in libraries like @ngrx/store and @angular/router.

7. Conclusion

Hierarchical Dependency Injection is a cornerstone of Angular’s design. It allows you to control how and where services are instantiated, providing flexibility for both small-scale and complex enterprise apps. By understanding the injector tree, using the right providedIn strategy, and scoping services carefully, you can create highly modular and efficient Angular applications.