← Back to Tutorials Chapter 7

Dependency Injection

What is Dependency Injection?

Dependency Injection (DI) is a design pattern where a class receives its dependencies from external sources rather than creating them internally. Angular has a built-in DI framework that provides dependencies to components, directives, pipes, and services. This makes your code more modular, testable, and maintainable.

@Injectable and providedIn

Services are decorated with @Injectable to make them available to the DI system. The providedIn property specifies where the service is registered:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'  // Singleton — one instance for the entire app
})
export class DataService {
  constructor(private http: HttpClient) { }

  getItems() {
    return this.http.get('/api/items');
  }
}
providedIn: 'root' creates a tree-shakeable singleton service available application-wide. If the service is never injected, it is tree-shaken from the bundle — reducing the final build size.

DI Patterns

Singleton Service

The most common pattern. A single instance is shared across the application. Use providedIn: 'root' or add the service to the providers array of the root module.

Factory Providers (useFactory)

Create a provider that uses a factory function to determine the dependency at runtime:

const loggerFactory = (http: HttpClient) => {
  return environment.production
    ? new RemoteLoggerService(http)
    : new ConsoleLoggerService();
};

@NgModule({
  providers: [
    { provide: LoggerService, useFactory: loggerFactory, deps: [HttpClient] }
  ]
})

useClass

Substitute one implementation with another:

{ provide: ApiService, useClass: MockApiService }

useExisting

Alias an existing provider to another token:

{ provide: OldLoggerService, useExisting: LoggerService }

useValue

Provide a static value or configuration object:

{ provide: 'API_URL', useValue: 'https://api.example.com/v2' }

Hierarchical DI

Angular's DI is hierarchical. Each component can have its own injector that inherits from its parent. This allows different instances of a service at different levels of the component tree.

@Component({
  selector: 'app-child',
  providers: [DataService]  // New instance for this component and its children
})

If a component does not define its own provider, it inherits from the parent injector, eventually reaching the root injector.

DI Decorators

@Optional

Marks a dependency as optional. If the dependency is not found, Angular injects null instead of throwing an error:

constructor(@Optional() private logger?: LoggerService) { }

@Self

Restricts resolution to the current component's injector only — does not check parent injectors:

constructor(@Self() private localService: LocalService) { }

@SkipSelf

Skips the current component's injector and resolves from the parent injector:

constructor(@SkipSelf() private parentService: ParentService) { }

@Host

Limits resolution to the host component's injector. Useful when a component is projected into another component via ng-content:

constructor(@Host() private hostService: HostService) { }
Angular dependency injection hierarchy

Tree-Shakeable Providers

When you use providedIn: 'root', Angular creates a tree-shakeable provider. If the service is never injected, it is removed from the production bundle. This is the recommended approach for most application-wide services.

@Injectable({
  providedIn: 'platform'  // Singleton across multiple Angular apps on the same page
})
export class SharedService { }

Available options: 'root', 'platform', 'any', or a specific module.

Injecting Services into Services

Services can inject other services through their constructor:

@Injectable({ providedIn: 'root' })
export class OrderService {
  constructor(
    private http: HttpClient,
    private authService: AuthService,
    private logger: LoggerService
  ) { }
}

Summary

You learned about Angular's dependency injection system, including @Injectable, providedIn, various provider patterns (useClass, useExisting, useValue, useFactory), hierarchical injectors, DI decorators (@Optional, @Self, @SkipSelf, @Host), and tree-shakeable providers. These tools help you design loosely coupled, testable services.

Exercise 7: Create a ConfigService with providedIn: 'root' that reads environment-specific settings. Create a FeatureFlagService that depends on ConfigService. Use useFactory to conditionally provide either a live AnalyticsService or a no-op MockAnalyticsService based on the environment. Use @Optional to inject a CacheService that might not be provided.