What is dependency injection in Angular?

HighIntermediateAngular
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
Preparing for interviews?

Use the relevant interview-question hub first, then move into a concrete study plan before targeted company sets.