Interview answer drill

Use this Angular interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.

What is dependency injection in Angular?Frontend interview answer

HighIntermediateAngular
Interview focus

This Angular interview question tests whether you can explain Angular dependency injection in production: providers, injector hierarchy, and scope bugs, connect it to production trade-offs, and handle common follow-up questions.

  • Angular dependency injection in production: providers, injector hierarchy, and scope bugs explanation without falling back to memorized docs wording
  • Dependency Injection and Services reasoning, edge cases, and production failure modes
  • How you would answer the most likely Angular interview follow-up
Practice more Angular interview questions
Interview 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 and Angular resolves them through a hierarchical injector tree. The senior follow-up is the real scope bug where a lower provider creates a second instance, plus knowing provider shapes and modifiers like Optional, Self, and SkipSelf.

Full interview 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.

Real bug pattern: closest provider wins

This is where DI stops being theory. A root-provided service can look “global” until a feature route or component provides the same token again. Then that subtree gets a different instance, which is exactly how carts, wizard state, and caches appear to reset “randomly” in production.

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();
}
                  
TYPESCRIPT
import { Component, Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class CartService {
  items = 3;
}

@Component({
  selector: 'app-checkout',
  standalone: true,
  providers: [CartService],
  template: `Checkout cart items: {{ cart.items }}`
})
export class CheckoutComponent {
  constructor(public cart: CartService) {}
}

// The header may still use the root CartService instance,
// while CheckoutComponent and its subtree now use a different one.
// That is why provider scope is a production bug source, not just an interview footnote.
                  

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).

Resolution modifiers seniors mention quickly

@Optional() says “return null if missing.” @Self() says “only check the current injector.” @SkipSelf() says “start at the parent injector.” These are the tools you use when the default nearest-provider rule is not the behavior you want.

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 deliberately override one provider in a test or feature subtree.

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, verify singleton behavior, and add one scope-override test when the service is stateful.

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 this as one explanation rep, then continue with the Angular interview questions cluster or a guided prep path.