Suggest an editImprove this articleRefine the answer for “Advanced dependency injection patterns in Angular”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Advanced dependency injection patterns in Angular** extend the DI system with multi-providers, factories, and injector hierarchy controls. ```typescript providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }, ] // Both interceptors run in chain, not overwriting each other ``` **Key:** `multi: true` collects, `useFactory` computes at resolution time, `@Self`/`@SkipSelf` scope the injector search.Shown above the full answer for quick recall.Answer (EN)Image**Advanced dependency injection patterns in Angular** use custom tokens, factory functions, multi-providers, and injector hierarchy decorators to build service architectures that go far beyond registering a single class. ## Theory ### TL;DR - Basic DI: one token, one instance. Advanced DI: arrays, factories, injector trees. - `multi: true` collects all providers for a token into an array. Without it, each new provider silently overwrites the previous one. - `useFactory` runs at resolution time, so you can call `inject()` inside for dynamic dependencies. - `@Self`, `@SkipSelf`, `@Optional` control which injector level Angular searches, not what it looks for. - Component-level providers create a new instance per component, not a shared singleton. ### Quick example Multi-provider for HTTP interceptors, the most common advanced DI pattern: ```typescript // app.config.ts providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }, ] // Injecting the token gives you an array, not a single instance constructor(@Inject(HTTP_INTERCEPTORS) private interceptors: HttpInterceptor[]) { // interceptors = [AuthInterceptor instance, LoggingInterceptor instance] } // Request chain: Auth -> Logging -> HttpClient ``` Without `multi: true`, the second provider overwrites the first. Angular does not warn you. The interceptor just disappears. ### Key difference Basic DI resolves one class or value per token. Advanced patterns change what "one" means. Multi-providers collect all matching entries into an array during injector tree traversal. Factory providers run a function at resolution time instead of calling `new`. Abstract class tokens let you swap implementations without touching consumer code. All three work inside the same hierarchical injector tree Angular builds at startup. ### When to use - Multiple handlers for the same concern (interceptors, validators, plugins) → `multi: true` - Runtime logic to pick an implementation (browser vs SSR, dev vs prod) → `useFactory` - Third-party objects or legacy code that can't be decorated with `@Injectable` → `useValue` or `useExisting` - Feature module needs its own instance, not the root singleton → component or route-level providers - Swappable implementations behind a stable contract → abstract class as token ### How Angular resolves dependencies Angular builds a tree of `Injector` instances at startup. The root is the platform injector, below it sits the root app injector, then module injectors, then component injectors. When you call `inject(Token)` or declare a constructor parameter, Angular walks up this tree from the current node until it finds a matching `provide` key. For multi-providers, it collects every matching entry from the entire walk and returns them as an array in registration order. For regular providers, it returns the first match on the way up. That is why a component-level provider shadows the root one for that component's subtree, but sibling components still see the root instance. ### Injection decorators: @Self, @SkipSelf, @Optional These three decorators tell Angular where to look. `@Self()` restricts the search to the current injector only. If the token is not there, Angular throws immediately. Use it when a service only makes sense at the component level, like a form controller that must be declared on the same element. `@SkipSelf()` does the opposite: it skips the current injector and starts the search from the parent. This lets child components access a parent's service while providing a different implementation to their own children. `@Optional()` returns `null` instead of throwing when nothing is found. It pairs well with analytics services or feature flags that are not always registered. ```typescript @Component({ providers: [FormService] }) export class FormComponent { // Throws if FormService is not in THIS component's injector constructor(@Self() private form: FormService) {} } @Component({}) export class ChildComponent { // Uses the parent's LoggerService, ignores any local override constructor(@SkipSelf() private logger: LoggerService) {} } ``` ### Abstract class as token TypeScript interfaces disappear at compile time, so they can't be DI tokens. Abstract classes survive compilation and produce real JavaScript constructor functions, which Angular can use as provider map keys. ```typescript abstract class StorageService { abstract get(key: string): string | null; abstract set(key: string, value: string): void; } @Injectable() class LocalStorageService extends StorageService { get(key: string) { return localStorage.getItem(key); } set(key: string, value: string) { localStorage.setItem(key, value); } } // In providers: { provide: StorageService, useClass: LocalStorageService } // In any consumer: constructor(private storage: StorageService) { this.storage.get('token'); // actual impl: LocalStorageService } ``` Swap `LocalStorageService` for `SessionStorageService` in one place. Nothing else changes. ### Common mistakes **Forgetting `multi: true` on HTTP interceptors** This is the most frequent DI bug I see in Angular codebases. Works fine with one interceptor, breaks silently when a second developer adds another. ```typescript // Wrong: second provider replaces the first providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor }, { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor }, // replaces Auth ] // Correct providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }, ] ``` **Calling `inject()` outside an injection context** ```typescript @Injectable() class BadService { private dep = inject(OtherService); // OK - field initializer runs during creation someMethod() { const dep = inject(OtherService); // Throws: inject() only works during construction } } ``` `inject()` works in field initializers, constructors, and `useFactory` functions. Anywhere else it throws at runtime. **Circular dependencies in factories** ```typescript { provide: A, useFactory: () => new A(inject(B)) } { provide: B, useFactory: () => new B(inject(A)) } // A needs B, B needs A ``` Angular detects cycles in dev mode and throws. In some prod builds it crashes without a clear message. Break the cycle by injecting `Injector` and resolving lazily, or restructure the dependency graph. **Expecting lazy-module providers to be visible in sibling routes** A lazy-loaded module creates its own child injector. Providers declared there are not visible to other routes. If you need a service shared across lazy routes, register it in the root injector with `providedIn: 'root'`. ### Real-world usage - Angular HttpClient uses `HTTP_INTERCEPTORS` as a multi-provider token out of the box - NGRX and NGXS register store plugins via custom multi-provider tokens - Nx monorepos use `useFactory` with `PLATFORM_ID` to provide env-specific config in SSR apps - `APP_CONFIG` injection tokens are standard in standalone Angular apps for feature flags and API URLs ### Follow-up questions **Q:** What is the difference between `useClass` and `useFactory`? **A:** `useClass` calls `new` on the class and lets Angular handle constructor injection automatically. `useFactory` runs a plain function at resolution time, which lets you write conditional logic and call `inject()` for additional dependencies inside the function. **Q:** How does `multi: true` interact with injector hierarchy? **A:** Each injector in the tree collects its own multi-provider entries. Child injectors add from their own registrations first, then from parents. The final array order matches registration order within each level. **Q:** When does Angular create a new service instance vs reuse one? **A:** Angular creates one instance per injector for any given token. `providedIn: 'root'` means one instance in the root injector, shared everywhere. A component-level provider means a new instance per component instance, destroyed when that component is destroyed. **Q:** Why can't you use a TypeScript interface as a DI token? **A:** Interfaces are erased at compile time. At runtime there is no object Angular can use as a key in its provider map. Abstract classes survive compilation because they produce real JavaScript constructor functions. **Q (senior):** In a lazy-loaded route where a `providedIn: 'root'` service is overridden via route-level providers, what does a sibling route get when it injects that service? **A:** The root instance. The override only applies to the lazy route's injector subtree. Sibling routes share the parent injector and never see the override. ## Examples ### Environment-based logger with useFactory ```typescript import { InjectionToken, PLATFORM_ID, inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; export const LOGGER = new InjectionToken<Logger>('LOGGER'); // In providers: { provide: LOGGER, useFactory: () => { const platformId = inject(PLATFORM_ID); return isPlatformBrowser(platformId) ? new ConsoleLogger() // Browser: logs to console : new NoopLogger(); // SSR: no output, avoids hydration issues } } // Usage in any service @Injectable() export class ApiService { private logger = inject(LOGGER); fetchData() { this.logger.log('Fetching data'); // Console in browser, silent in SSR } } ``` The factory runs once per injector at creation time and picks the right implementation based on the runtime environment. No `if` statements scattered through consumer code. ### APP_CONFIG token for feature flags ```typescript export interface AppConfig { apiUrl: string; debug: boolean; } export const APP_CONFIG = new InjectionToken<AppConfig>('app.config'); // main.ts bootstrapApplication(AppComponent, { providers: [ { provide: APP_CONFIG, useValue: { apiUrl: environment.apiUrl, debug: !environment.production, } } ] }); // Any service or component @Injectable({ providedIn: 'root' }) export class ApiService { private config = inject(APP_CONFIG); getUrl(path: string) { if (this.config.debug) console.log(`Calling ${this.config.apiUrl}/${path}`); return `${this.config.apiUrl}/${path}`; } } ``` `InjectionToken` with `useValue` is the standard way to pass static config through the DI system without creating a service class for it. ### Hierarchical override in a lazy-loaded feature ```typescript // Root provides a global ApiService @Injectable({ providedIn: 'root' }) export class ApiService { baseUrl = '/api'; } // Feature service extends root behavior @Injectable() export class FeatureApiService extends ApiService { baseUrl = '/api/feature'; } // Lazy route overrides ApiService only within its injector subtree const routes: Routes = [ { path: 'feature', loadComponent: () => import('./feature.component').then(m => m.FeatureComponent), providers: [{ provide: ApiService, useClass: FeatureApiService }] } ]; // FeatureComponent and its children get FeatureApiService // All other routes still get the root ApiService ``` This is the pattern for multi-tenant dashboards or white-label apps where different sections need different API behavior but share the same consumer code.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.