Skip to content

Angular Dependency Injection Explained — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Angular Dependency Injection Explained. We cover key concepts, practical examples, and best practices to help you master this topic.

Angular dependency injection (DI) is a design pattern where a class receives its dependencies from an external source rather than creating them internally, making code more modular and testable.

What You'll Learn

  • How Angular's DI system works
  • The difference between providers and injectors
  • How the hierarchical injector tree works
  • How to use @Injectable, @Inject, and injection tokens
  • How to control service scope with providedIn

Why It Matters

DI is the foundation of Angular's architecture. It lets you swap implementations, mock services in tests, and share instances across your application without tight coupling between classes.

Real-World Use

In Durga Antivirus Pro, a LoggerService is injected into every component. During development it logs to the console; in production, a different provider writes to a secure file. The components never change — DI handles the switch.

flowchart TD
    A[App Injector] --> B[Root Services]
    B --> C[Component A]
    B --> D[Component B]
    C --> E[Child Injector]
    E --> F[Child Component]
    style A fill:#f97316,color:#fff

What is Dependency Injection?

Without DI, a class creates its own dependencies:

class Car {
  engine = new Engine();
  tires = new Tires();
}

This is rigid. If Engine changes its constructor, Car breaks. If you want to test Car with a mock engine, you cannot.

With DI, the dependencies are injected from outside:

class Car {
  constructor(private engine: Engine, private tires: Tires) {}
}

Angular creates and provides the Engine and Tires instances. Car only declares what it needs.

Hierarchical Injectors

Angular has a tree of injectors that mirrors the component tree:

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

@Injectable({
  providedIn: "root"
})
export class ConfigService {
  private config = { apiUrl: "https://api.example.com", timeout: 5000 };

  getConfig() {
    return this.config;
  }
}

Services provided at the root level are shared across the entire app. If you provide a service at the component level, each component instance gets its own copy:

@Component({
  selector: "app-editor",
  standalone: true,
  providers: [FormDraftService],
  template: `...`
})
export class EditorComponent {}

Expected output: Each EditorComponent instance gets a separate FormDraftService, so drafts do not leak between editors.

Angular first looks for the service in the component's injector, then in the parent component's injector, and finally in the root injector. The first match wins.

Injection Tokens

Use InjectionToken when you need to provide values that are not classes, like configuration objects:

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

export interface AppConfig {
  apiUrl: string;
  appName: string;
  version: string;
}

export const APP_CONFIG = new InjectionToken<AppConfig>("app.config");

Provide the value in the app configuration:

import { ApplicationConfig } from "@angular/core";
import { APP_CONFIG } from "./app-config";

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_CONFIG,
      useValue: {
        apiUrl: "https://api.example.com",
        appName: "My Angular App",
        version: "1.0.0"
      }
    }
  ]
};

Inject the token in components:

import { Component, Inject } from "@angular/core";
import { APP_CONFIG, AppConfig } from "./app-config";

@Component({
  selector: "app-root",
  standalone: true,
  template: `<h1>{{ config.appName }} v{{ config.version }}</h1>`
})
export class AppComponent {
  constructor(@Inject(APP_CONFIG) public config: AppConfig) {}
}

Expected output: "My Angular App v1.0.0" rendered on the page.

The @Inject decorator tells Angular which token to resolve. The string passed to InjectionToken is a description used for debugging.

Factory Providers

Use useFactory when the service needs dynamic configuration:

import { Injectable, inject } from "@angular/core";

export function storageFactory() {
  const prefix = "app_";
  return {
    get: (key: string) => localStorage.getItem(prefix + key),
    set: (key: string, value: string) => localStorage.setItem(prefix + key, value),
    remove: (key: string) => localStorage.removeItem(prefix + key),
  };
}

export const STORAGE = new InjectionToken<ReturnType<typeof storageFactory>>("storage");

Provision:

providers: [
  { provide: STORAGE, useFactory: storageFactory }
]

Expected output: A storage service with automatic key prefixing, created via a factory function.

Factory providers let you create instances with complex setup, conditional logic, or configuration that is not available at import time.

UseExisting and UseClass

useExisting creates an alias, while useClass substitutes one class for another:

// Production logger
class ProductionLogger {
  log(message: string) { console.log("[PROD]", message); }
}

// Development logger
class DevelopmentLogger {
  log(message: string) { console.log("[DEV]", message); }
}

// Provide based on environment
providers: [
  {
    provide: DevelopmentLogger,
    useClass: isProduction ? ProductionLogger : DevelopmentLogger
  }
]

Expected output: In development, DevelopmentLogger is used. In production, ProductionLogger replaces it transparently.

This is how Angular apps switch between development and production behavior. The component that injects the logger never knows which implementation it receives.

Common Mistakes

  1. Providing the same service at multiple levels — If you provide a service in a child component, the parent and child get different instances. This is often unintended.

  2. Circular dependencies — Service A injects service B, which injects service A. Use forwardRef or restructure to break the cycle.

  3. Forgetting @Inject for non-class tokens — Injection tokens and primitive values require @Inject() in the constructor.

  4. Overusing component-level providers — Most services should be root-scoped. Only use component-level providers for genuinely isolated state.

  5. Services with too many constructor parameters — If a service needs 5+ dependencies, consider grouping related dependencies into a single configuration object.

Practice Questions

  1. What is the purpose of providedIn: "root"? It makes the service available app-wide as a Singleton without adding it to any module's providers.

  2. How does Angular resolve a dependency? It looks up the injector tree: first the component's injector, then its parent, then the root injector.

  3. What is an InjectionToken used for? It is a unique key for providing non-class values like configuration objects, functions, or strings.

  4. What is the difference between useClass and useExisting? useClass creates a new instance of a different class. useExisting aliases an existing provider.

  5. When would you use a factory provider? When creating the service requires runtime logic, configuration, or conditional instantiation.

Challenge

Build a PermissionService that checks user roles. Create an AuthConfig injection token with roles and adminEmails. Use a factory provider to initialize the permission service from the config. Inject it into a component that conditionally shows admin features.

FAQ

What happens if no provider is found for a dependency?

Angular throws a "NullInjectorError: No provider for ..." error at runtime.

Can I inject a service into another service?

Yes, Angular's DI supports constructor injection across all injectable classes.

What is the `@Optional()` decorator in DI?

It marks a dependency as optional. If no provider exists, Angular injects null instead of throwing an error.

What is the injector hierarchy?

The injector tree mirrors the component tree. Root injector -> feature module injectors -> component injectors.

How do I test a component with dependencies?

Use TestBed.configureTestingModule({ providers: [MockService] }) to provide mock implementations.

Mini Project

Build an AnalyticsDashboardComponent that uses an AnalyticsService with a factory provider. The factory should accept a configuration object (via InjectionToken) that specifies the API endpoint and refresh interval. Create a mock analytics Adapter for development and a real HTTP client adapter for production. The dashboard should show real-time stats using whichever adapter is injected.

What's Next

Continue learning about the component lifecycle and change detection:

Angular Lifecycle, Angular Change Detection, Angular Services

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro