What is dependency injection in Angular?

LowIntermediateAngular
Preparing for interviews?

Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.

Quick Answer

Angular dependency injection (DI) is the framework mechanism that creates and supplies dependencies (services/config/values) to classes (components, directives, pipes, other services) based on providers. Instead of constructing dependencies manually, you register providers (how to build a value for a token) and Angular resolves them through a hierarchical injector tree. Interview focus: provider configuration, token types (class vs InjectionToken), scoping/lifetime (root vs component), and common pitfalls (multiple instances, circular deps, Optional/Self/SkipSelf). DI affects testability and architecture; test with mocks and scope providers carefully.

Answer

Core idea

DI is how Angular finds and creates what your class needs. You request a dependency by token (usually a class), and Angular returns an instance/value based on the closest matching provider in the injector hierarchy.

Term

What it is

What interviewers expect you to say

Token

The lookup key for a dependency (class type or InjectionToken).

“Angular resolves dependencies by token; classes are tokens, but for primitives/objects you use InjectionToken.”

Provider

A rule that tells Angular how to produce a value for a token (useClass/useValue/useFactory/useExisting).

“Providers map token → value/instance creation strategy.”

Injector

A container that holds providers and can resolve tokens. Injectors form a tree.

“Resolution walks up the injector tree; the closest provider wins.”

Scope / lifetime

Where the provider is registered determines whether you get one shared instance or many.

“Root providers are app-singletons; component providers create per-component instances.”

DI vocabulary (the minimum you should be fluent with)

How resolution works (hierarchical)

When Angular needs a dependency, it checks the current injector (e.g., component injector), and if not found it walks up to parent injectors until it finds a provider. If multiple levels provide the same token, the nearest one is used.

Where you provide

Instance behavior

Typical use

@Injectable({ providedIn: 'root' })

One instance for the whole app (tree-shakeable).

Most services (API clients, facades, shared utilities).

providers: [...] on a component

New instance per component instance (and its subtree).

Per-screen/wizard state that must reset on destroy.

Route/environment providers (standalone bootstrap)

Scoped to an environment injector (app-wide or route subtree).

App configuration + feature-level provider scoping in standalone apps.

Scope/lifetime is a top Angular interview hotspot
TYPESCRIPT
import { Component, Injectable, inject } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class UsersService {
  getUsers() { return ['Ada', 'Linus']; }
}

@Component({
  selector: 'app-users',
  standalone: true,
  template: `{{ users.join(', ') }}`
})
export class UsersComponent {
  // Option A: constructor injection
  // constructor(private usersService: UsersService) {}

  // Option B: inject() (same timing category as constructor)
  private readonly usersService = inject(UsersService);

  users = this.usersService.getUsers();
}
                  

Providers: the 4 shapes you must know

These are the practical knobs that control what DI returns for a token.

Provider type

What it does

When to use

useClass

Instantiate a class when the token is requested.

Swap implementations (e.g., mock vs real).

useValue

Return a constant value/object.

Config objects, feature flags, small immutable values.

useFactory

Call a factory function (can depend on other injections).

Computed config, environment-dependent setup.

useExisting

Alias one token to another (same instance).

Expose one implementation under multiple tokens.

Provider shapes
TYPESCRIPT
import { InjectionToken } from '@angular/core';

export type AppConfig = { apiBaseUrl: string };
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');

// Example provider (standalone bootstrap or module providers)
// { provide: APP_CONFIG, useValue: { apiBaseUrl: '/api' } }
                  
TYPESCRIPT
import { Component, Inject, Injectable } from '@angular/core';
import { APP_CONFIG, AppConfig } from './app-config.token';

@Injectable({ providedIn: 'root' })
export class ApiClient {
  constructor(@Inject(APP_CONFIG) private cfg: AppConfig) {}

  url(path: string) {
    return `${this.cfg.apiBaseUrl}${path}`;
  }
}

@Component({
  selector: 'app-demo',
  standalone: true,
  template: `...`,
  providers: [
    { provide: APP_CONFIG, useValue: { apiBaseUrl: '/v2' } }
  ]
})
export class DemoComponent {
  constructor(public api: ApiClient) {}
}
                  

Multi providers (common in real apps)

Some tokens accept multiple values. Angular collects them into an array when multi: true is used (classic example: HTTP interceptors).

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

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authReq = req.clone({ setHeaders: { Authorization: 'Bearer token' } });
  return next(authReq);
};

// provider
// { provide: HTTP_INTERCEPTORS, useValue: authInterceptor, multi: true }
                  

Common pitfall

Symptom

Fix

Accidentally creating multiple instances

Stateful service resets unexpectedly or differs between components.

Understand scope: root vs component providers; avoid providing the same service at many component levels unless you want per-instance state.

Using the wrong token for non-classes

Runtime DI error: “No provider for X” (or injecting string/Object doesn’t work).

Use InjectionToken for primitives/config and inject via @Inject(TOKEN).

Circular dependencies

Runtime error or partially-initialized services.

Refactor responsibilities; extract shared logic; consider factory indirection only as a last resort.

Optional dependency not marked

DI throws when provider is absent.

Use @Optional() (or provide a default via factory/value).

Resolution surprises in a hierarchy

You think you’re getting the root singleton, but a child injector overrides it.

Know “closest provider wins”; use @SkipSelf()/@Self() intentionally when needed.

Pitfalls seniors mention quickly

Practical scenario
Inject a logging service and an API client across multiple features, then swap a mock in tests.

Common pitfalls

      • Providing a service at the wrong level and creating extra instances.
      • Circular dependencies causing runtime errors.
      • Hard-coding dependencies that make testing hard.
Trade-off or test tip
DI improves testability but needs clear provider scope. Test with mocks and verify singleton behavior.

Summary

Angular DI resolves dependencies by token using providers in a hierarchical injector tree (closest provider wins). Providers define how to create a value (useClass/useValue/useFactory/useExisting). Where you provide controls lifetime (root singleton vs component-scoped). For non-class deps use InjectionToken. Multi providers collect values into arrays (e.g., interceptors). Common pitfalls are accidental multiple instances, wrong tokens, and circular deps.

Similar questions
Guides
30 / 37