A simple pattern that keeps your Angular app modular, scalable, and sane.
Why I Needed It
A few months ago, I was working on a large Angular 20 project. It had everything: analytics, error monitoring, A/B testing, feature flags, and more integrations than I could count.
Every new service wanted to āinitializeā itself at startup.
Soon, my main.ts looked like a spaghetti monster of async calls and environment checks.
I knew there had to be a cleaner way.
Thatās when I revisited something most Angular devs overlook: multi-providers.
āļø The Hidden Power of Multi-Providers
Angularās DI system can do more than inject single services.
With a multi provider, you can register multiple implementations under one InjectionToken.
When you inject that token,ā¦
A simple pattern that keeps your Angular app modular, scalable, and sane.
Why I Needed It
A few months ago, I was working on a large Angular 20 project. It had everything: analytics, error monitoring, A/B testing, feature flags, and more integrations than I could count.
Every new service wanted to āinitializeā itself at startup.
Soon, my main.ts looked like a spaghetti monster of async calls and environment checks.
I knew there had to be a cleaner way.
Thatās when I revisited something most Angular devs overlook: multi-providers.
āļø The Hidden Power of Multi-Providers
Angularās DI system can do more than inject single services.
With a multi provider, you can register multiple implementations under one InjectionToken.
When you inject that token, you get an array of all registered items, perfect for a plugin system.
This pattern lets you add or remove features without touching the core codebase.
Each plugin is just a class with an init() method and a unique ID.
Step 1 ā Define the Plugin Contract
// core/plugins/plugin.token.ts
import { InjectionToken } from '@angular/core';
export interface AppPlugin {
readonly id: string;
readonly order?: number;
isEnabled?(): boolean;
init(): void | Promise<void>;
}
export const APP_PLUGINS = new InjectionToken<AppPlugin[]>('app.plugins');
Thatās it. A minimal interface. Each plugin knows how to initialize itself, and Angular will collect them all through this token.
Step 2 ā A Registry to Run Them All
We need one lightweight service to coordinate everything at startup.
// core/plugins/plugin-registry.service.ts
import { Injectable, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { APP_PLUGINS, AppPlugin } from './plugin.token';
@Injectable({ providedIn: 'root' })
export class PluginRegistry {
private readonly plugins = inject(APP_PLUGINS, { optional: true }) ?? [];
private readonly platformId = inject(PLATFORM_ID);
async initAll(): Promise<void> {
if (!isPlatformBrowser(this.platformId)) return; // skip SSR
const eligible = this.plugins
.filter(p => p.isEnabled?.() ?? true)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
for (const p of eligible) {
try {
await Promise.resolve(p.init());
console.info(`[Plugin] ${p.id} initialized`);
} catch (err) {
console.error(`[Plugin] ${p.id} failed`, err);
}
}
}
}
In one of my apps, this registry replaced nearly 200 lines of manual startup logic. Now, every integration just registers itself and runs automatically.
Step 3 ā Bootstrap Cleanly with provideAppInitializer
Angular 20 introduced provideAppInitializer(), a small but powerful helper that replaces boilerplate APP_INITIALIZER factories.
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAppInitializer, inject } from '@angular/core';
import { AppComponent } from './app/app.component';
import { PluginRegistry } from './core/plugins/plugin-registry.service';
import { APP_PLUGINS } from './core/plugins/plugin.token';
import { SentryPlugin } from './core/plugins/sentry.plugin';
import { GoogleAnalyticsPlugin } from './core/plugins/ga.plugin';
bootstrapApplication(AppComponent, {
providers: [
{ provide: APP_PLUGINS, useClass: SentryPlugin, multi: true },
{ provide: APP_PLUGINS, useClass: GoogleAnalyticsPlugin, multi: true },
provideAppInitializer(() => inject(PluginRegistry).initAll()),
]
});
One line replaces all the āinit this, then thatā chaos, and it runs safely before your root component renders.
Step 4 ā Real Plugins in Action
Hereās how the plugins look in practice. Each one is self-contained and only loads if itās actually enabled.
// core/plugins/ga.plugin.ts
import { Injectable } from '@angular/core';
import { AppPlugin } from './plugin.token';
@Injectable({ providedIn: 'root' })
export class GoogleAnalyticsPlugin implements AppPlugin {
readonly id = 'ga4';
readonly order = 10;
isEnabled() {
return !!(window as any).ENV?.GA_MEASUREMENT_ID;
}
async init() {
const id = (window as any).ENV.GA_MEASUREMENT_ID;
if (!id) return;
await this.loadScript(`https://www.googletagmanager.com/gtag/js?id=${id}`);
(window as any).dataLayer = (window as any).dataLayer || [];
function gtag(...args: any[]) { (window as any).dataLayer.push(args); }
(window as any).gtag = gtag;
gtag('js', new Date());
gtag('config', id, { anonymize_ip: true });
}
private loadScript(src: string) {
return new Promise<void>((resolve, reject) => {
const s = document.createElement('script');
s.async = true;
s.src = src;
s.onload = () => resolve();
s.onerror = reject;
document.head.appendChild(s);
});
}
}
// core/plugins/sentry.plugin.ts
import { Injectable } from '@angular/core';
import { AppPlugin } from './plugin.token';
@Injectable({ providedIn: 'root' })
export class SentryPlugin implements AppPlugin {
readonly id = 'sentry';
readonly order = 5;
isEnabled() {
return !!(window as any).ENV?.SENTRY_DSN;
}
async init() {
const dsn = (window as any).ENV.SENTRY_DSN;
if (!dsn) return;
const Sentry = await import('@sentry/browser');
Sentry.init({ dsn, tracesSampleRate: 0.1 });
}
}
In production, both run automatically, no imports, no conditionals, no spaghetti.
Step 5 ā Feature-Scoped Plugins
This pattern scales nicely across domains. A payments library, for example, can register its own plugin without touching the core app:
// libs/payments/payment.plugin.ts
import { APP_PLUGINS, AppPlugin } from '@app/core/plugins/plugin.token';
import { Provider } from '@angular/core';
class PaymentsAuditPlugin implements AppPlugin {
readonly id = 'payments-audit';
init() { /* custom logic */ }
}
export const providePaymentsPlugins: Provider[] = [
{ provide: APP_PLUGINS, useClass: PaymentsAuditPlugin, multi: true }
];
Attach it right in the route config:
{
path: 'payments',
providers: [providePaymentsPlugins],
loadComponent: () => import('./payments.component').then(m => m.PaymentsComponent)
}
Now every feature can extend global behavior independently. No central bottlenecks.
ā” What This Gives You
From experience, this small pattern delivers huge wins:
- Extensibility: add or remove integrations safely
- Stability: a broken plugin canāt crash the app
- SSR friendly: browser-only code stays browser-side
- Testable: mock any plugin easily in unit tests
- Maintainable: cross-cutting logic lives in one place
Why It Matters
In one of our enterprise apps, we had six different analytics SDKs, all fighting for control of window.dataLayer.
After moving to this plugin registry, we bootstrapped them cleanly, logged failures, and never touched them again.
Multi-providers are the unsung hero of Angularās DI system. They turn a monolith into a composable frontend, with zero external libraries and full type safety.
Looking Ahead
Angularās DI has been rock-solid for years, and it keeps improving around developer experience and performance. The good news is: the multi-provider pattern isnāt going anywhere. Itās stable, fast, and perfectly aligned with Angularās standalone architecture.
š¬ Final Thoughts
If youāve ever scaled an Angular app across multiple teams, you know how startup logic can spiral out of control. This pattern wonāt just clean it up, itāll future-proof it.
Give it a try in your next project. Youāll never go back to manual āinitā scripts again.
Author: Anastasios Theodosiou Senior Software Engineer | Angular Certified Developer | Building Scalable Frontend Systems
If you found this useful, follow for more deep dives into real-world Angular architecture.