Appearance
ADR-003: Module Federation for Micro-Frontend Architecture
Status
Accepted
Date
2023-Q2
Context
The Microtec ERP frontend needs to deliver 10 distinct business modules: Accounting, HR, Finance, Sales, Purchase, Inventory, Distribution, Fixed Assets, Business Owners portal, and an ERP home shell. Each module is owned by a different feature team, has an independent release cadence, and is expected to grow to tens of thousands of lines of code.
The initial approach was a single Angular workspace with lazy-loaded route modules. This quickly ran into limitations:
- A change in any module required testing and deploying the entire application
- Build times grew as all modules were compiled together
- Teams could not deploy independently — a broken module in the Accounting build blocked the HR release
- Bundle size was unconstrained; importing a large library in one module loaded it for all users
We evaluated three micro-frontend approaches:
| Option | Description | Key Concerns |
|---|---|---|
| iFrame | Each module runs in an isolated iframe | Poor UX (scroll, resize, auth sharing), high overhead, no shared state |
| single-spa | Framework-agnostic MFE orchestrator | Framework-agnostic (good), but adds indirection; Angular adapter needed |
| Module Federation | Webpack 5 native, Angular native support | First-class Angular support via @angular-architects/module-federation |
Decision
Adopt Webpack Module Federation via @angular-architects/module-federation.
Each of the 10 Angular applications in FrontApps/projects/ is an independently deployed and independently built Module Federation remote. The ERP home shell (erp-home, port 4401) acts as the host that dynamically loads remote modules at runtime.
Architecture
FrontApps/projects/
├── erp-home/ ← Host shell (port 4401) — loads all remotes
├── apps-accounting/ ← Remote (port 4402)
├── apps-hr/ ← Remote (port 4403)
├── apps-finance/ ← Remote (port 4404)
├── apps-sales/ ← Remote (port 4405)
├── apps-purchase/ ← Remote (port 4406)
├── apps-inventory/ ← Remote (port 4407)
├── app-distribution/ ← Remote (port 4408)
├── fixed-assets/ ← Remote (port 4409)
└── bussiness-owners/ ← Standalone (port 4301, separate Keycloak realm)Shared Libraries
Singleton dependencies (Angular core, RxJS, Keycloak-js, PrimeNG) are declared as shared in each app's webpack.config.js. Module Federation ensures each singleton loads once across all remotes:
javascript
// webpack.config.js (each remote)
shared: share({
"@angular/core": { singleton: true, strictVersion: true, requiredVersion: "auto" },
"@angular/router": { singleton: true, strictVersion: true, requiredVersion: "auto" },
"rxjs": { singleton: true, strictVersion: true, requiredVersion: "auto" },
"primeng": { singleton: true, requiredVersion: "auto" },
"keycloak-js": { singleton: true, requiredVersion: "auto" },
})Shared UI components and services live in FrontApps/libs/shared-lib.
Dynamic Remote Loading
The shell loads remotes using runtime manifest URLs — no compile-time coupling:
typescript
// erp-home: loadRemoteModule
const routes: Routes = [
{
path: 'accounting',
loadChildren: () => loadRemoteModule({
remoteEntry: environment.remotes.accounting, // e.g., https://accounting.microtecstage.com/remoteEntry.js
remoteName: 'apps_accounting',
exposedModule: './AccountingModule'
}).then(m => m.AccountingModule)
}
];Remote entry URLs are environment-specific and configured in environment.{env}.ts files.
Consequences
Positive
- Independent deployment: The HR team deploys
apps-hrwithout touching any other module - Independent build times: Each module builds in ~2 minutes independently vs ~15 minutes for the full monolith
- Team autonomy: Feature teams have full ownership of their remote — routes, state, components
- Incremental loading: Users only download the JavaScript for modules they visit
- Shared singletons: Angular, RxJS, and PrimeNG load once — no duplicate frameworks
- Graceful degradation: If one remote is unavailable, the shell can show an error boundary instead of crashing entirely
Negative
- Version coordination required: If
@angular/coreversions diverge between remotes, singleton sharing breaks at runtime. Teams must coordinate Angular version upgrades. - Webpack configuration complexity: Each app needs a
webpack.config.jswith carefulshareddeclarations. A misconfiguration causes silent duplicate-module bugs. - Local development overhead: Running all 10 apps simultaneously requires significant RAM. Use
npm run start:erpto run only the apps you need. - Build system coupling: Module Federation is tightly coupled to Webpack. Migrating to Vite/esbuild would require significant rework.
- SSR complexity: Server-side rendering with Module Federation requires additional orchestration (relevant for the Fooj project, not ERP).
Neutral
@angular-architects/module-federationis the community-standard wrapper and tracks Angular releases closely- The
bussiness-ownersapp is deliberately separate (different Keycloak realm) and does not federate with ERP home
Implementation Notes
Angular Version Policy
All remotes must use the same Angular major version. Minor/patch updates can be staggered but must be completed within one sprint of the host shell upgrade.
| Rule | Enforcement |
|---|---|
Same @angular/core major | Enforced in package.json peer deps of shared-lib |
Same rxjs major | Enforced in root package.json |
| PrimeNG version | Synced manually during Angular upgrades |
Port Assignments (Local Development)
| App | Port |
|---|---|
bussiness-owners | 4301 |
erp-home (shell) | 4401 |
apps-accounting | 4402 |
apps-hr | 4403 |
apps-finance | 4404 |
apps-sales | 4405 |
apps-purchase | 4406 |
apps-inventory | 4407 |
app-distribution | 4408 |
fixed-assets | 4409 |
Deployment Topology
In production, each remote is deployed to Azure Static Web Apps at a subdomain. The remote entry URL configured in the shell's environment file points to the production SWA URL.
Related ADRs
- ADR-001: Microservices Architecture (parallel to MFE decomposition on the frontend)
- ADR-004: Keycloak SSO (shared auth across all MFE remotes)
- ADR-005: Azure Service Bus (not directly related, but same decomposition philosophy)