Skip to content

Dependency Injection in Angular

What is Dependency Injection?

Dependency Injection (DI) is a design pattern and core feature of Angular that allows you to supply dependencies to a class rather than creating them inside the class. Angular's DI system makes your code more modular, testable, and maintainable.

Core Concepts

Dependencies and Dependents

// UserService is a dependency
@Injectable()
export class UserService {
  getUsers() {
    return ['John', 'Jane', 'Bob'];
  }
}

// UserComponent is the dependent
@Component({
  selector: 'app-user',
  template: '<ul><li *ngFor="let user of users">{{ user }}</li></ul>'
})
export class UserComponent {
  users: string[];

  // UserService is injected into the constructor
  constructor(private userService: UserService) {
    this.users = this.userService.getUsers();
  }
}

Injection Tokens

Angular uses injection tokens to identify dependencies:

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

// Creating an injection token
export const API_URL = new InjectionToken<string>('api.url');
export const CONFIG = new InjectionToken<AppConfig>('app.config');

// Using injection tokens
@Component({
  selector: 'app-config-display'
})
export class ConfigDisplayComponent {
  constructor(
    @Inject(API_URL) private apiUrl: string,
    @Inject(CONFIG) private config: AppConfig
  ) {
    console.log('API URL:', apiUrl);
    console.log('Config:', config);
  }
}

Creating Injectable Services

Basic Service

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

@Injectable({
  providedIn: 'root'  // Makes it a singleton available app-wide
})
export class LoggingService {
  private logs: string[] = [];

  log(message: string) {
    const timestamp = new Date().toISOString();
    const logEntry = `[${timestamp}] ${message}`;
    this.logs.push(logEntry);
    console.log(logEntry);
  }

  getLogs(): string[] {
    return [...this.logs];
  }

  clearLogs() {
    this.logs = [];
  }
}

Service with Dependencies

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface User {
  id: number;
  name: string;
  email: string;
}

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private apiUrl = 'https://jsonplaceholder.typicode.com/users';

  constructor(
    private http: HttpClient,
    private loggingService: LoggingService
  ) {}

  getUsers(): Observable<User[]> {
    this.loggingService.log('Fetching users from API');
    return this.http.get<User[]>(this.apiUrl);
  }

  getUser(id: number): Observable<User> {
    this.loggingService.log(`Fetching user with id: ${id}`);
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }

  createUser(user: Partial<User>): Observable<User> {
    this.loggingService.log('Creating new user');
    return this.http.post<User>(this.apiUrl, user);
  }
}

Provider Configuration

Root Level Providers

Using providedIn

@Injectable({
  providedIn: 'root'  // Singleton across the entire app
})
export class GlobalService {
  // Service implementation
}

Using providers in main.ts (Standalone)

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  providers: [
    GlobalService,
    { provide: LoggingService, useClass: LoggingService },
    { provide: API_URL, useValue: 'https://api.example.com' }
  ]
});

Using providers in AppModule

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

Component Level Providers

@Component({
  selector: 'app-user-profile',
  providers: [
    UserProfileService,  // New instance for each component instance
    { provide: COMPONENT_ID, useValue: 'user-profile-123' }
  ],
  template: `
    <h2>User Profile</h2>
    <p>Profile ID: {{ profileId }}</p>
  `
})
export class UserProfileComponent {
  profileId: string;

  constructor(
    private userProfileService: UserProfileService,
    @Inject(COMPONENT_ID) componentId: string
  ) {
    this.profileId = componentId;
  }
}

Provider Types

Class Providers

// Basic class provider
{ provide: UserService, useClass: UserService }

// Alternative implementation
{ provide: UserService, useClass: MockUserService }

// Abstract class implementation
abstract class BaseApiService {
  abstract getData(): Observable<any>;
}

@Injectable()
class ApiService extends BaseApiService {
  getData(): Observable<any> {
    // Implementation
  }
}

// Provider configuration
{ provide: BaseApiService, useClass: ApiService }

Value Providers

const appConfig = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
};

// Value provider
{ provide: 'APP_CONFIG', useValue: appConfig }

// Using in component
constructor(@Inject('APP_CONFIG') private config: any) {
  console.log('API URL:', config.apiUrl);
}

Factory Providers

// Factory function
function createLogger(isDevelopment: boolean): LoggingService {
  if (isDevelopment) {
    return new ConsoleLoggingService();
  } else {
    return new RemoteLoggingService();
  }
}

// Factory provider
{
  provide: LoggingService,
  useFactory: createLogger,
  deps: [IS_DEVELOPMENT]  // Dependencies for the factory function
}

// Complex factory example
function createHttpClient(apiUrl: string, authService: AuthService): HttpClient {
  const httpClient = new HttpClient();
  // Configure httpClient with interceptors, base URL, etc.
  return httpClient;
}

{
  provide: 'CONFIGURED_HTTP_CLIENT',
  useFactory: createHttpClient,
  deps: [API_URL, AuthService]
}

Existing Provider

// Use existing provider
{ provide: 'LOGGER', useExisting: LoggingService }

// Useful for aliasing
{ provide: 'USER_API', useExisting: UserService }

Injection Strategies

Constructor Injection

@Component({
  selector: 'app-dashboard'
})
export class DashboardComponent {
  constructor(
    private userService: UserService,
    private loggingService: LoggingService,
    @Inject(API_URL) private apiUrl: string
  ) {}
}

Optional Dependencies

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

@Component({
  selector: 'app-feature'
})
export class FeatureComponent {
  constructor(
    private requiredService: RequiredService,
    @Optional() private optionalService?: OptionalService
  ) {
    if (this.optionalService) {
      // Use optional service if available
      this.optionalService.doSomething();
    }
  }
}

Self and SkipSelf

import { Self, SkipSelf } from '@angular/core';

@Component({
  selector: 'app-parent',
  providers: [SharedService]
})
export class ParentComponent {}

@Component({
  selector: 'app-child'
})
export class ChildComponent {
  constructor(
    @Self() private selfService: SharedService,      // Only from this component
    @SkipSelf() private parentService: SharedService  // Skip this component, look in parent
  ) {}
}

Host Decorator

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

@Component({
  selector: 'app-child'
})
export class ChildComponent {
  constructor(
    @Host() private hostService: HostService  // Only from host component
  ) {}
}

Multi Providers

Collecting Multiple Providers

// Define multi-provider token
export const PLUGIN_TOKEN = new InjectionToken<Plugin[]>('plugins');

// Register multiple providers
export const PLUGIN_PROVIDERS = [
  { provide: PLUGIN_TOKEN, useClass: PluginA, multi: true },
  { provide: PLUGIN_TOKEN, useClass: PluginB, multi: true },
  { provide: PLUGIN_TOKEN, useClass: PluginC, multi: true }
];

// Use in component
@Component({
  providers: [PLUGIN_PROVIDERS]
})
export class PluginManagerComponent {
  constructor(@Inject(PLUGIN_TOKEN) private plugins: Plugin[]) {
    // All plugins are available as an array
    plugins.forEach(plugin => plugin.initialize());
  }
}

HTTP Interceptors Example

import { HTTP_INTERCEPTORS } from '@angular/common/http';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Add auth header
    const authReq = req.clone({
      headers: req.headers.set('Authorization', 'Bearer token')
    });
    return next.handle(authReq);
  }
}

// Provider configuration
{
  provide: HTTP_INTERCEPTORS,
  useClass: AuthInterceptor,
  multi: true  // Allows multiple interceptors
}

Hierarchical Injection

Understanding the Injector Tree

AppModule Injector (Root)
├── FeatureModule Injector
│   ├── FeatureComponent Injector
│   └── AnotherComponent Injector
└── SharedModule Injector
    └── SharedComponent Injector

Resolution Strategy

Angular searches for providers in this order:

  1. Current component's injector
  2. Parent component's injector
  3. Module injector
  4. Root injector

Example of Hierarchical DI

// Root level service
@Injectable({ providedIn: 'root' })
export class GlobalCounterService {
  count = 0;
  increment() { this.count++; }
}

// Module level service
@Injectable()
export class ModuleCounterService {
  count = 0;
  increment() { this.count++; }
}

// Feature module
@NgModule({
  providers: [ModuleCounterService]
})
export class FeatureModule {}

// Component with its own provider
@Component({
  selector: 'app-counter',
  providers: [ModuleCounterService],  // Component-specific instance
  template: `
    <div>
      <p>Global Count: {{ globalCounter.count }}</p>
      <p>Module Count: {{ moduleCounter.count }}</p>
      <button (click)="increment()">Increment</button>
    </div>
  `
})
export class CounterComponent {
  constructor(
    public globalCounter: GlobalCounterService,   // Shared instance
    public moduleCounter: ModuleCounterService    // Component-specific instance
  ) {}

  increment() {
    this.globalCounter.increment();
    this.moduleCounter.increment();
  }
}

Service Scoping

Singleton Services

// Root singleton - shared across entire app
@Injectable({ providedIn: 'root' })
export class SingletonService {
  private data: any[] = [];

  addData(item: any) {
    this.data.push(item);
  }

  getData() {
    return this.data;
  }
}

Module-Scoped Services

// Service scoped to a module
@Injectable()
export class FeatureScopedService {
  // New instance for each module that imports it
}

@NgModule({
  providers: [FeatureScopedService]
})
export class FeatureModule {}

Component-Scoped Services

// Service scoped to component and its children
@Injectable()
export class ComponentScopedService {
  // New instance for each component that provides it
}

@Component({
  providers: [ComponentScopedService]
})
export class MyComponent {}

Advanced DI Patterns

Service Locator Pattern

@Injectable({ providedIn: 'root' })
export class ServiceLocator {
  constructor(private injector: Injector) {}

  getService<T>(token: any): T {
    return this.injector.get(token);
  }
}

// Usage
@Component({})
export class DynamicComponent {
  constructor(private serviceLocator: ServiceLocator) {
    const userService = this.serviceLocator.getService(UserService);
  }
}

Dynamic Service Creation

import { Injector, ComponentFactoryResolver } from '@angular/core';

@Injectable()
export class DynamicServiceFactory {
  constructor(private injector: Injector) {}

  createService<T>(serviceClass: new(...args: any[]) => T): T {
    const service = this.injector.get(serviceClass);
    return service;
  }
}

Conditional Providers

import { environment } from '../environments/environment';

// Conditional provider based on environment
export const LOGGER_PROVIDER = {
  provide: LoggingService,
  useClass: environment.production ? ProductionLogger : DevelopmentLogger
};

// Feature flag provider
export const FEATURE_PROVIDERS = environment.enableBetaFeatures
  ? [BetaFeatureService, ExperimentalService]
  : [StandardFeatureService];

Testing with Dependency Injection

Mocking Dependencies

import { TestBed } from '@angular/core/testing';

describe('UserComponent', () => {
  let component: UserComponent;
  let mockUserService: jasmine.SpyObj<UserService>;

  beforeEach(() => {
    const spy = jasmine.createSpyObj('UserService', ['getUsers', 'getUser']);

    TestBed.configureTestingModule({
      declarations: [UserComponent],
      providers: [
        { provide: UserService, useValue: spy }
      ]
    });

    mockUserService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
  });

  it('should call getUserService', () => {
    mockUserService.getUsers.and.returnValue(of(['test user']));
    component.loadUsers();
    expect(mockUserService.getUsers).toHaveBeenCalled();
  });
});

Testing Providers

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService]
    });

    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  it('should fetch users', () => {
    const mockUsers = [{ id: 1, name: 'John' }];

    service.getUsers().subscribe(users => {
      expect(users).toEqual(mockUsers);
    });

    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers);
  });
});

Best Practices

1. Use Interface Segregation

// Define focused interfaces
interface UserReader {
  getUser(id: number): Observable<User>;
  getUsers(): Observable<User[]>;
}

interface UserWriter {
  createUser(user: User): Observable<User>;
  updateUser(user: User): Observable<User>;
  deleteUser(id: number): Observable<void>;
}

// Implement specific interfaces
@Injectable()
export class UserService implements UserReader, UserWriter {
  // Implementation
}

// Inject specific interfaces where needed
@Component({})
export class UserListComponent {
  constructor(private userReader: UserReader) {}  // Only needs read operations
}

2. Avoid Circular Dependencies

// Bad - circular dependency
@Injectable()
export class ServiceA {
  constructor(private serviceB: ServiceB) {}
}

@Injectable()
export class ServiceB {
  constructor(private serviceA: ServiceA) {}
}

// Good - use shared interface or event system
export interface EventBus {
  emit(event: string, data: any): void;
  on(event: string, callback: Function): void;
}

@Injectable()
export class ServiceA {
  constructor(private eventBus: EventBus) {}
}

@Injectable()
export class ServiceB {
  constructor(private eventBus: EventBus) {}
}

3. Use Abstract Classes for Service Contracts

export abstract class ApiService {
  abstract get<T>(url: string): Observable<T>;
  abstract post<T>(url: string, data: any): Observable<T>;
}

@Injectable()
export class HttpApiService extends ApiService {
  constructor(private http: HttpClient) {
    super();
  }

  get<T>(url: string): Observable<T> {
    return this.http.get<T>(url);
  }

  post<T>(url: string, data: any): Observable<T> {
    return this.http.post<T>(url, data);
  }
}

// Provider
{ provide: ApiService, useClass: HttpApiService }

4. Lazy Service Loading

@Injectable({
  providedIn: 'root'
})
export class LazyServiceLoader {
  private services = new Map<string, Promise<any>>();

  async loadService<T>(modulePath: string, serviceName: string): Promise<T> {
    if (!this.services.has(serviceName)) {
      const modulePromise = import(modulePath).then(module => {
        return module[serviceName];
      });
      this.services.set(serviceName, modulePromise);
    }
    return this.services.get(serviceName)!;
  }
}

Common Pitfalls

1. Overusing Injection

// Bad - too many dependencies
@Component({})
export class OverloadedComponent {
  constructor(
    private service1: Service1,
    private service2: Service2,
    private service3: Service3,
    private service4: Service4,
    private service5: Service5
  ) {}
}

// Good - use facade pattern
@Injectable()
export class ComponentFacade {
  constructor(
    private service1: Service1,
    private service2: Service2,
    private service3: Service3
  ) {}
}

@Component({})
export class CleanComponent {
  constructor(private facade: ComponentFacade) {}
}

2. Memory Leaks with Singleton Services

// Bad - holds references
@Injectable({ providedIn: 'root' })
export class LeakyService {
  private components: any[] = [];

  registerComponent(component: any) {
    this.components.push(component);  // Memory leak!
  }
}

// Good - use WeakMap or cleanup
@Injectable({ providedIn: 'root' })
export class CleanService {
  private components = new WeakMap();

  registerComponent(component: any, data: any) {
    this.components.set(component, data);
  }
}

Summary

Angular's Dependency Injection system provides:

  • Inversion of Control: Dependencies are provided rather than created
  • Testability: Easy to mock dependencies for testing
  • Modularity: Services can be swapped without changing consumers
  • Hierarchy: Different scopes for different use cases
  • Flexibility: Multiple provider types and configuration options

Key concepts:

  • Services are classes with @Injectable() decorator
  • Providers configure how dependencies are created
  • Injection hierarchy determines service scope
  • Multiple provider types support different scenarios
  • Proper testing requires dependency mocking

Next Steps

Now that you understand Dependency Injection, you're ready to explore:

  1. Creating and using Angular services
  2. HTTP client and API communication
  3. State management patterns
  4. Advanced service patterns
  5. Testing strategies

Dependency Injection is the foundation of Angular's architecture - master it to build maintainable and testable applications!