As enterprise engineering organizations scale, single monolithic codebases often become bottlenecks. Build times skyrocket, deployment schedules collide, and team velocity slows.
This case study analyzes how we partitioned a large fintech platform into Angular Micro-Frontends using Webpack Module Federation, enabling multiple autonomous teams to deploy code independently.
The Architectural Goal
The objective was to separate a corporate banking dashboard into three independent deployables:
- Shell (Host): Responsible for layout, global navigation, session management, and routing.
- KYC & Onboarding (Remote): A secure wizard module for corporate verification workflows.
- Transactional Ledger (Remote): A high-performance transaction dashboard and charting panel.
1. Webpack Module Federation Configuration
Webpack Module Federation allows an application to dynamically load compiled code from a remote server at runtime. In Angular, we configure this using custom builders.
Shell (Host) Configuration
The Shell configuration declares the dynamic remotes and exposes shared libraries:
// webpack.config.js (Shell/Host)
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
plugins: [
new ModuleFederationPlugin({
remotes: {
"onboarding": "onboarding@http://localhost:4201/remoteEntry.js",
},
shared: {
"@angular/core": { singleton: true, strictVersion: true },
"@angular/common": { singleton: true, strictVersion: true },
"@angular/router": { singleton: true, strictVersion: true }
}
})
]
};
Remote (Onboarding) Configuration
The remote module compiles its features and exposes them to the host:
// webpack.config.js (Remote/Onboarding)
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "onboarding",
filename: "remoteEntry.js",
exposes: {
"./Module": "./src/app/onboarding/onboarding.module.ts"
},
shared: {
"@angular/core": { singleton: true, strictVersion: true },
"@angular/common": { singleton: true, strictVersion: true },
"@angular/router": { singleton: true, strictVersion: true }
}
})
]
};
2. Dynamic Routing in the Host
In the Shell router, we load the remote module dynamically using Webpack’s import statement, wrapped in Angular’s router load children hook:
import { Routes } from '@angular/router';
import { loadRemoteModule } from '@angular-architects/module-federation';
export const APP_ROUTES: Routes = [
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
},
{
path: 'onboarding',
loadChildren: () =>
loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:4201/remoteEntry.js',
exposedModule: './Module'
}).then(m => m.OnboardingModule)
}
];
3. Resolving Shared State Challenges
One of the major pitfalls of micro-frontend designs is state pollution. Sharing store states directly across remotes tightly couples the deployments.
To solve this, we implemented a Message Bus architecture using RxJS. The shell exposes a thin event bus, and remotes communicate exclusively by publishing and subscribing to serializable events:
import { Injectable } from '@angular/core';
import { Subject, filter } from 'rxjs';
export interface AppEvent {
type: string;
payload: any;
}
@Injectable({ providedIn: 'root' })
export class EventBusService {
private eventSubject = new Subject<AppEvent>();
publish(event: AppEvent) {
this.eventSubject.next(event);
}
on(eventType: string) {
return this.eventSubject.asObservable().pipe(
filter(event => event.type === eventType)
);
}
}
Conclusion
Transitioning to Angular Micro-Frontends allowed us to reduce build and deploy loops from 40 minutes to under 3 minutes per team. By defining strict API boundaries and using a lightweight event bus for state updates, we achieved complete deployment isolation without sacrificing performance.