Angular Dependency Injection (DI)

Learn Angular Dependency Injection (DI) in detail including Injector hierarchy, providers, inject(), InjectionToken, hierarchical DI, standalone APIs, architecture diagrams, lifecycle, best practices, enterprise examples, and interview questions.

Dependency Injection (DI) is one of Angular's core architectural features. It enables Angular to automatically create and supply the objects (dependencies) that a class requires instead of having the class create them itself.

Without DI, components become tightly coupled to concrete implementations. With DI, applications become:

  • Loosely coupled
  • Easier to test
  • Easier to maintain
  • More reusable
  • Highly scalable

Dependency Injection is one of the most frequently asked Angular interview topics, especially for Senior Developers, Tech Leads, and Architects.


Table of Contents

  • What is Dependency Injection?
  • Why Dependency Injection?
  • Inversion of Control (IoC)
  • DI Architecture
  • Injectable Services
  • Providers
  • Injector Hierarchy
  • inject() Function
  • InjectionToken
  • Multi Providers
  • Hierarchical Dependency Injection
  • Standalone Applications
  • Enterprise Example
  • Best Practices
  • Top 10 Interview Questions
  • Summary

What is Dependency Injection?

Dependency Injection is a design pattern where an external object provides the dependencies needed by a class.

Instead of this:

export class UserComponent {

  service = new UserService();

}

Angular does this automatically:

constructor(
  private userService: UserService
) {}

Angular creates the service and injects it.


Why Dependency Injection?

Without DI

  • Tight coupling
  • Difficult testing
  • Duplicate objects
  • Hard to replace implementations

With DI

  • Loose coupling
  • Easier unit testing
  • Singleton services
  • Better maintainability
  • Better scalability

Dependency Injection Architecture

flowchart LR

Component

Component --> Injector

Injector --> UserService

Injector --> LoggerService

Injector --> ConfigService

UserService --> API

LoggerService --> Console

ConfigService --> Environment

Dependency Injection Flow

sequenceDiagram

participant Component

participant Injector

participant Service

participant API

Component->>Injector: Need UserService

Injector->>Service: Create or Retrieve Instance

Service->>API: Fetch Data

API-->>Service: Response

Service-->>Component: Data

What is Inversion of Control (IoC)?

Normally, a class creates its own dependencies.

const service = new UserService();

With IoC

Angular Framework

↓

Injector

↓

Creates Service

↓

Provides Service

↓

Component

The control of object creation is inverted and managed by Angular.


Service Example

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

@Injectable({
  providedIn: 'root'
})
export class UserService {

  getUsers() {
    return ['Venu', 'John', 'David'];
  }

}

Injecting a Service

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

@Component({
  selector: 'app-users',
  template: `
    <ul>
      <li *ngFor="let user of users">
        {{ user }}
      </li>
    </ul>
  `
})
export class UsersComponent {

  users = this.userService.getUsers();

  constructor(
    private userService: UserService
  ) {}

}

Service Creation Flow

flowchart TD

Component

Component --> Constructor

Constructor --> Injector

Injector --> UserService

UserService --> Component

@Injectable Decorator

The @Injectable() decorator tells Angular that a class can participate in Dependency Injection.

@Injectable({
  providedIn: 'root'
})
export class ProductService {}

Benefits

  • Registers the service
  • Enables dependency injection
  • Supports tree shaking

providedIn Options

Option Scope
'root' Singleton application-wide service
'platform' Shared across Angular applications on the same page
'any' New instance for each lazy-loaded environment (legacy option; use with care)
Specific Injector Register with a specific injector or environment

Provider Registration

Angular allows providers at multiple levels.

flowchart TD

Application

Application --> RootInjector

RootInjector --> FeatureInjector

FeatureInjector --> ComponentInjector

ComponentInjector --> Component

Root Provider

@Injectable({
  providedIn: 'root'
})
export class AuthService {}

One shared instance for the application.


Component Provider

@Component({
  selector: 'app-user',
  providers: [UserService]
})
export class UserComponent {}

Every component instance receives its own UserService.


Provider Hierarchy

flowchart TD

RootInjector

RootInjector --> FeatureInjector

FeatureInjector --> ComponentInjector

ComponentInjector --> DirectiveInjector

Angular resolves dependencies by searching from the closest injector upward.


Injector Hierarchy Resolution

flowchart LR

Component

Component --> ComponentInjector

ComponentInjector --> Found

ComponentInjector --> RootInjector

RootInjector --> FoundService

Constructor Injection

The most common approach.

constructor(

private authService: AuthService,

private logger: LoggerService

){}

Angular injects both services automatically.


Using inject() Function

Angular also supports the inject() function.

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

@Component({
  standalone: true,
  template: `{{ users.length }}`
})
export class UserComponent {

  private userService = inject(UserService);

  users = this.userService.getUsers();

}

Benefits

  • Cleaner syntax
  • No constructor required
  • Useful in standalone APIs
  • Convenient for functional patterns

inject() Flow

flowchart LR
    A["inject()"]
    B["Injector"]
    C["Service"]
    D["Component"]

    A --> B
    B --> C
    C --> D

InjectionToken

Used when injecting interfaces, configuration objects, or primitive values.

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

export const API_URL =
new InjectionToken<string>('API_URL');

Provider

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

Injection

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

constructor(
  @Inject(API_URL)
  private apiUrl: string
) {}

Why InjectionToken?

Interfaces disappear after TypeScript compilation.

interface PaymentService {}

Angular cannot inject interfaces directly.

Instead

const PAYMENT_SERVICE =
new InjectionToken<PaymentService>(
'PaymentService'
);

Provider Types

Provider Purpose
useClass Create an instance of a class
useValue Inject a constant value
useExisting Alias another provider
useFactory Create dynamically
multi Multiple implementations

useClass Example

providers: [
{
provide: LoggerService,
useClass: ConsoleLoggerService
}
]

useValue Example

providers: [
{
provide: API_URL,
useValue: 'https://api.company.com'
}
]

useFactory Example

export function loggerFactory() {
  return new LoggerService();
}

providers: [
{
provide: LoggerService,
useFactory: loggerFactory
}
]

useExisting Example

providers: [
{
provide: OldLogger,
useExisting: LoggerService
}
]

Multi Providers

Multiple services can share the same token.

providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: LoggingInterceptor,
multi: true
}
]

Angular injects an array of interceptors.


Multi Provider Architecture

flowchart LR

Injector

Injector --> AuthInterceptor

Injector --> LoggingInterceptor

Injector --> CacheInterceptor

AuthInterceptor --> HTTPRequest

LoggingInterceptor --> HTTPRequest

CacheInterceptor --> HTTPRequest

Hierarchical Dependency Injection

Angular maintains a hierarchy of injectors.

flowchart TD

RootInjector

RootInjector --> AdminModule

RootInjector --> UserModule

AdminModule --> AdminComponent

UserModule --> ProfileComponent

Benefits

  • Scoped services
  • Better memory usage
  • Independent feature modules
  • Easier testing

Standalone Application Example

import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient()
  ]
});

Modern Angular encourages provider registration during application bootstrap for standalone applications.


Enterprise Banking Example

flowchart TD

Dashboard

Dashboard --> AccountService

Dashboard --> AuthService

Dashboard --> NotificationService

Dashboard --> LoggerService

AccountService --> RESTAPI

AuthService --> OAuth

NotificationService --> Email

LoggerService --> Monitoring

Component

constructor(

private accountService: AccountService,

private authService: AuthService,

private logger: LoggerService

){}

Performance Best Practices

Practice Benefit
Use providedIn: 'root' Tree shaking and singleton instances
Avoid unnecessary component providers Reduce duplicate instances
Use inject() where appropriate Cleaner standalone code
Prefer interfaces with InjectionToken Better abstraction
Use factories for runtime configuration Flexible initialization
Scope services intentionally Better resource management

Common Mistakes

Creating Services with new

❌ Avoid

const service = new UserService();

Always let Angular manage service creation.


Registering Services Everywhere

Registering the same service in multiple component providers may create unnecessary instances.


Injecting Interfaces Directly

Interfaces are not available at runtime.

Use InjectionToken instead.


Circular Dependencies

Avoid services that depend on each other directly.

A

↓

B

↓

A

Refactor shared logic into another service if needed.


Advantages

Feature Benefit
Loose Coupling Easier maintenance
Testability Simple mocking
Singleton Support Shared instances
Hierarchical Injectors Scoped services
Tree Shaking Smaller bundles
Standalone APIs Modern Angular development

Top 10 Angular Dependency Injection Interview Questions

1. What is Dependency Injection?

Answer

Dependency Injection is a design pattern where Angular creates and supplies required dependencies instead of allowing classes to create them directly.


2. Why is Dependency Injection important?

Answer

It promotes loose coupling, improves maintainability, simplifies testing, and enables reusable services.


3. What is the purpose of @Injectable()?

Answer

@Injectable() marks a class as available for Angular's Dependency Injection system and allows it to declare injectable dependencies.


4. What does providedIn: 'root' mean?

Answer

It registers the service with the application's root injector, creating a singleton instance shared throughout the application.


5. What is an Injector?

Answer

An Injector is responsible for creating, storing, and supplying service instances to components, directives, pipes, and other services.


6. What is the difference between constructor injection and inject()?

Constructor Injection inject()
Uses constructor parameters Uses the inject() function
Traditional approach Useful in standalone APIs and functional code
Easy to understand Reduces constructor boilerplate in some cases

7. What is an InjectionToken?

Answer

An InjectionToken provides a runtime token for injecting values, interfaces, or configuration objects that cannot be identified by class type alone.


8. What are the different provider types?

Answer

  • useClass
  • useValue
  • useExisting
  • useFactory
  • multi

9. What are Multi Providers?

Answer

Multi providers allow multiple values or implementations to be associated with the same injection token. A common example is HTTP_INTERCEPTORS.


10. What are the best practices for Dependency Injection?

Answer

  • Prefer providedIn: 'root' for application-wide services.
  • Avoid manually creating services with new.
  • Use InjectionToken for interfaces and configuration.
  • Scope providers intentionally.
  • Use inject() where it improves readability.
  • Avoid circular dependencies.
  • Register providers only where appropriate.

Interview Cheat Sheet

Topic Remember
DI Angular supplies dependencies
Main Decorator @Injectable()
Root Provider providedIn: 'root'
Modern API inject()
Runtime Token InjectionToken
Provider Types useClass, useValue, useExisting, useFactory, multi
Injector Creates and provides services
Singleton Root Injector
Component Scope Component providers
Multi Provider Example HTTP_INTERCEPTORS

Summary

Dependency Injection is a foundational concept in Angular that enables loosely coupled, maintainable, and testable applications. By using Angular's hierarchical injector system, services can be shared globally or scoped to specific features and components. Modern Angular enhances DI with features like inject(), InjectionToken, standalone application providers, and tree-shakable services, making Dependency Injection one of the most powerful features of the framework.


Key Takeaways

  • ✔ Dependency Injection promotes loose coupling.
  • ✔ Angular manages object creation through Injectors.
  • @Injectable() enables classes to participate in DI.
  • providedIn: 'root' creates application-wide singleton services.
  • ✔ Use inject() in modern Angular where appropriate.
  • ✔ Use InjectionToken for interfaces and configuration values.
  • ✔ Understand provider types: useClass, useValue, useExisting, useFactory, and multi.
  • ✔ Leverage hierarchical injectors for proper service scoping.
  • ✔ Avoid manual service creation and circular dependencies.
  • ✔ Dependency Injection is one of the most important Angular interview topics.

Next Article

➡️ 32-Service-Lifecycle-QA.md