# Advanced Angular Dependency Injection in 2026: Providers, Tokens and Interview Questions > Master Angular dependency injection with InjectionToken, hierarchical injectors, resolution modifiers, and multi-providers. Includes interview questions and production patterns. - Published: 2026-09-13 - Updated: 2026-09-13 - Author: Anthony Fillion-Maillet - Tags: angular, dependency-injection, typescript, interview - Reading time: 12 min --- Angular's dependency injection system provides fine-grained control over service instantiation through providers, tokens, and hierarchical injectors. Mastering these concepts separates experienced Angular developers from juniors in technical interviews and production codebases. > **Key Concept** > > Angular maintains two parallel injector trees: the ModuleInjector tree for services provided at module level, and the ElementInjector tree for component-scoped dependencies. Resolution starts at the element level and bubbles up to the root. ## Provider Configuration Strategies in Angular DI Angular offers multiple provider configuration strategies, each suited to different use cases. The most common configurations use `useClass`, `useValue`, `useFactory`, and `useExisting`. ```typescript // app.config.ts import { ApplicationConfig, InjectionToken } from '@angular/core'; import { LoggerService } from './services/logger.service'; import { DebugLoggerService } from './services/debug-logger.service'; import { API_CONFIG, ApiConfig } from './config/api.config'; export const appConfig: ApplicationConfig = { providers: [ // useClass: provide a different implementation { provide: LoggerService, useClass: DebugLoggerService }, // useValue: provide a static configuration object { provide: API_CONFIG, useValue: { baseUrl: 'https://api.example.com', timeout: 5000 } }, // useFactory: create dependency with runtime logic { provide: 'FEATURE_FLAGS', useFactory: () => { const env = import.meta.env.MODE; return { debugMode: env === 'development', analytics: env === 'production' }; } }, // useExisting: create an alias to another provider { provide: 'Logger', useExisting: LoggerService } ] }; ``` The `useClass` strategy swaps implementations without changing consumer code. The `useValue` strategy provides static objects like configuration. The `useFactory` strategy handles runtime decisions, and `useExisting` creates aliases for polymorphic access. ## InjectionToken for Type-Safe Non-Class Dependencies While `@Injectable` works for class-based services, non-class values like configuration objects, primitives, or functions require `InjectionToken`. This token acts as a unique key in Angular's DI registry. ```typescript // tokens/config.tokens.ts import { InjectionToken } from '@angular/core'; export interface ApiConfig { baseUrl: string; timeout: number; retryAttempts: number; } // Generic parameter ensures type safety at injection point export const API_CONFIG = new InjectionToken('api.config', { providedIn: 'root', factory: () => ({ baseUrl: 'https://api.sharpskill.dev', timeout: 30000, retryAttempts: 3 }) }); // Token for primitive values export const MAX_UPLOAD_SIZE = new InjectionToken('max.upload.size', { providedIn: 'root', factory: () => 10 * 1024 * 1024 // 10MB }); ``` The generic type parameter on `InjectionToken` propagates to the `inject()` call. TypeScript knows the injected value matches the declared type, catching misuse at compile time rather than runtime. ## The inject() Function vs Constructor Injection Angular 14 introduced the `inject()` function as an alternative to constructor-based injection. In Angular 20+, `inject()` has become the preferred approach, especially in standalone components and functional contexts. ```typescript // user.service.ts import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { API_CONFIG } from '../tokens/config.tokens'; @Injectable({ providedIn: 'root' }) export class UserService { // Modern approach: inject() at field level private readonly http = inject(HttpClient); private readonly config = inject(API_CONFIG); getUser(id: string) { return this.http.get(`${this.config.baseUrl}/users/${id}`); } } // Alternative: constructor injection (still valid) @Injectable({ providedIn: 'root' }) export class UserServiceLegacy { constructor( private readonly http: HttpClient, @Inject(API_CONFIG) private readonly config: ApiConfig ) {} } ``` The `inject()` function eliminates decorator boilerplate for tokens and enables dependency injection in non-class contexts like functional guards, resolvers, and interceptors. ```typescript // auth.guard.ts import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; import { AuthService } from '../services/auth.service'; // Functional guard using inject() export const authGuard: CanActivateFn = () => { const authService = inject(AuthService); const router = inject(Router); if (authService.isAuthenticated()) { return true; } return router.createUrlTree(['/login']); }; ``` The `inject()` function only works within an injection context: during class construction, in factory functions, or in functional Angular constructs. Calling it outside these contexts throws a runtime error. ## Hierarchical Injectors: ElementInjector vs EnvironmentInjector Angular maintains two parallel injector hierarchies that determine service scope and resolution order. Understanding this architecture is essential for controlling service lifetime and visibility. ```typescript // data.service.ts import { Injectable } from '@angular/core'; // Root-level singleton: single instance across entire app @Injectable({ providedIn: 'root' }) export class GlobalDataService { private data = new Map(); set(key: string, value: unknown) { this.data.set(key, value); } get(key: string) { return this.data.get(key); } } // Component-scoped: new instance per component @Injectable() export class ComponentDataService { private data = new Map(); set(key: string, value: unknown) { this.data.set(key, value); } get(key: string) { return this.data.get(key); } } ``` ```typescript // dashboard.component.ts import { Component } from '@angular/core'; import { ComponentDataService } from './services/component-data.service'; @Component({ selector: 'app-dashboard', standalone: true, // This component and all children get the same instance providers: [ComponentDataService], template: ` ` }) export class DashboardComponent {} ``` When a component declares a provider, Angular creates a new instance scoped to that component's ElementInjector. Child components inherit access to the parent's providers unless they declare their own. The resolution algorithm follows this path: 1. Check the requesting component's ElementInjector 2. Walk up the ElementInjector tree to ancestors 3. Check the EnvironmentInjector (module or standalone providers) 4. Walk up to the root EnvironmentInjector 5. Reach NullInjector and throw an error if `@Optional()` was not used ## Resolution Modifiers: @Self, @SkipSelf, @Optional, @Host Resolution modifiers alter how Angular searches the injector hierarchy. These decorators work with both constructor injection and the `inject()` function. ```typescript // panel.component.ts import { Component, Optional, SkipSelf, Self, inject } from '@angular/core'; import { PanelService } from './panel.service'; @Component({ selector: 'app-panel', standalone: true, providers: [PanelService], template: `` }) export class PanelComponent { // @Self: only look in this component's injector, fail otherwise private readonly localService = inject(PanelService, { self: true }); // @SkipSelf: skip this component, start search from parent private readonly parentService = inject(PanelService, { skipSelf: true, optional: true }); // @Optional: return null instead of throwing if not found private readonly optionalService = inject(PanelService, { optional: true }); constructor() { // localService is always the component's own instance // parentService is the parent's instance or null console.log('Local:', this.localService); console.log('Parent:', this.parentService); } } ``` The `@Host()` modifier restricts resolution to the host element's injector and stops at component boundaries. This is useful when a directive needs to access a service provided by its host component but should not reach higher in the tree. ```typescript // highlight.directive.ts import { Directive, inject, Host, Optional } from '@angular/core'; import { HighlightConfig } from './highlight.config'; @Directive({ selector: '[appHighlight]', standalone: true }) export class HighlightDirective { // Only look at the host component's providers private readonly config = inject(HighlightConfig, { host: true, optional: true }); constructor() { // config is null if host component didn't provide HighlightConfig const color = this.config?.color ?? 'yellow'; // Apply highlighting... } } ``` ## Multi-Providers for Extensible Systems Multi-providers allow multiple values to be registered under a single token. Angular returns all registered values as an array, enabling plugin architectures and extensibility patterns. ```typescript // validators.tokens.ts import { InjectionToken } from '@angular/core'; export interface Validator { validate(value: string): string | null; } export const VALIDATORS = new InjectionToken('validators'); ``` ```typescript // app.config.ts import { ApplicationConfig } from '@angular/core'; import { VALIDATORS } from './validators.tokens'; const requiredValidator = { validate: (value: string) => value ? null : 'Field is required' }; const minLengthValidator = { validate: (value: string) => value.length >= 3 ? null : 'Minimum 3 characters' }; const emailValidator = { validate: (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : 'Invalid email format' }; export const appConfig: ApplicationConfig = { providers: [ { provide: VALIDATORS, useValue: requiredValidator, multi: true }, { provide: VALIDATORS, useValue: minLengthValidator, multi: true }, { provide: VALIDATORS, useValue: emailValidator, multi: true } ] }; ``` ```typescript // validation.service.ts import { Injectable, inject } from '@angular/core'; import { VALIDATORS, Validator } from './validators.tokens'; @Injectable({ providedIn: 'root' }) export class ValidationService { private readonly validators = inject(VALIDATORS); validate(value: string): string[] { // validators is an array of all registered validators return this.validators .map(v => v.validate(value)) .filter((error): error is string => error !== null); } } ``` The `multi: true` flag tells Angular to collect all providers for this token into an array. Without it, later providers would override earlier ones. ## Interview Questions on Angular Dependency Injection Technical interviews frequently probe understanding of Angular's DI system. Here are questions that distinguish candidates with production experience. **Q: What happens when you provide the same service at both module and component level?** The component-level provider creates a separate instance scoped to that component's subtree. Services injected in that subtree receive the component's instance, not the module-level singleton. This enables state isolation, for example when each tab needs its own form state. **Q: Why use `InjectionToken` instead of a string literal?** String tokens risk collisions across libraries or different parts of an application. `InjectionToken` creates a unique runtime reference that cannot conflict. The generic type parameter also provides compile-time type safety that string tokens lack. **Q: When does `inject()` throw vs return undefined?** By default, `inject()` throws when the dependency is not found. Passing `{ optional: true }` changes the return type to `T | null` and returns null instead of throwing. This matches the behavior of the `@Optional()` decorator. **Q: Explain the difference between `providedIn: 'root'` and providing in a module's providers array.** Both create singletons, but `providedIn: 'root'` enables tree-shaking. The service is only included in the bundle if actually injected somewhere. Module-level providers are always included regardless of usage. **Q: How does lazy loading affect service scope?** Lazy-loaded modules get their own child EnvironmentInjector. Services provided in a lazy module are scoped to that module and its children. A service with `providedIn: 'root'` remains a true singleton across all modules, lazy or not. For practice questions on Angular services and DI patterns, check [Angular interview questions on services and dependency injection](/technologies/angular/interview-questions/services-dependency-injection). ## Practical Patterns: Configuration and Feature Flags Real applications combine these DI concepts for configuration management. This pattern uses `InjectionToken`, factory providers, and environment awareness. ```typescript // feature-flags.config.ts import { InjectionToken, inject, PLATFORM_ID } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; export interface FeatureFlags { newCheckout: boolean; darkMode: boolean; betaFeatures: boolean; } export const FEATURE_FLAGS = new InjectionToken('feature.flags', { providedIn: 'root', factory: () => { const platformId = inject(PLATFORM_ID); if (!isPlatformBrowser(platformId)) { // SSR: return safe defaults return { newCheckout: false, darkMode: false, betaFeatures: false }; } // Browser: check localStorage or remote config const stored = localStorage.getItem('featureFlags'); if (stored) { return JSON.parse(stored); } return { newCheckout: true, darkMode: false, betaFeatures: false }; } }); ``` ```typescript // feature-flag.directive.ts import { Directive, Input, TemplateRef, ViewContainerRef, inject } from '@angular/core'; import { FEATURE_FLAGS } from './feature-flags.config'; @Directive({ selector: '[appFeatureFlag]', standalone: true }) export class FeatureFlagDirective { private readonly flags = inject(FEATURE_FLAGS); private readonly templateRef = inject(TemplateRef); private readonly viewContainer = inject(ViewContainerRef); @Input() set appFeatureFlag(flag: keyof typeof this.flags) { if (this.flags[flag]) { this.viewContainer.createEmbeddedView(this.templateRef); } else { this.viewContainer.clear(); } } } ``` This directive conditionally renders content based on feature flags, with the flag configuration centralized in one injectable token. ## Production-Ready Angular DI Practices - Use `providedIn: 'root'` for application-wide singletons that benefit from tree-shaking - Prefer `inject()` over constructor injection in Angular 20+ for cleaner syntax and functional compatibility - Scope stateful services to components when isolation is needed, not at module level - Create `InjectionToken` for non-class dependencies to ensure type safety and avoid collisions - Apply `@Optional()` when a dependency might not exist, especially for plugins or optional features - Test components with overridden providers using `TestBed.overrideComponent()` for isolation - Use multi-providers for extensibility patterns like validators, interceptors, and handlers --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/angular/angular-dependency-injection-advanced-providers-tokens-interview