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.
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
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
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 | “Angular resolves dependencies by token; classes are tokens, but for primitives/objects you use |
Provider | A rule that tells Angular how to produce a value for a token ( | “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.” |
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 |
|---|---|---|
| One instance for the whole app (tree-shakeable). | Most services (API clients, facades, shared utilities). |
| 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. |
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();
}
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 |
|---|---|---|
| Instantiate a class when the token is requested. | Swap implementations (e.g., mock vs real). |
| Return a constant value/object. | Config objects, feature flags, small immutable values. |
| Call a factory function (can depend on other injections). | Computed config, environment-dependent setup. |
| Alias one token to another (same instance). | Expose one implementation under multiple tokens. |
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' } }
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.
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 | Use |
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 |
Resolution surprises in a hierarchy | You think you’re getting the root singleton, but a child injector overrides it. | Know “closest provider wins”; use |
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.
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.
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.
Use this as one explanation rep, then continue with the Angular interview questions cluster or a guided prep path.