Module 21: Advanced Module Federation Configurations (Senior/Enterprise)
Chapter 21 • Advanced
Advanced Module Federation Configurations (Senior/Enterprise)
The Real Problem
Scenario: You have 20 micro frontends. Some are on different Angular versions. Some need conditional loading. Some have version conflicts.
The Question: How do I dynamically load remotes, handle version conflicts, and configure Module Federation for enterprise scale?
This is senior-level architecture.
The Production Reality
Advanced Requirements
- Dynamic Remote Loading - Load remotes at runtime, not build time
- Version Management - Handle different Angular versions
- Conditional Remotes - Load remotes based on feature flags
- Runtime Configuration - Change remote URLs without rebuild
- Shared Dependency Strategies - Advanced sharing rules
The Question: How do we solve each requirement?
Strategy 1: Dynamic Remote Loading
The Problem
You want to load remotes at runtime, not hardcode them at build time.
The Solution: Runtime Remote Registration
// Host App - Dynamic remote loader
@Injectable({ providedIn: 'root' })
export class DynamicRemoteLoader {
private loadedRemotes: Map<string, any> = new Map()
async loadRemote(name: string, url: string): Promise<any> {
// Check if already loaded
if (this.loadedRemotes.has(name)) {
return this.loadedRemotes.get(name)
}
// Load remote entry
const container = await this.loadRemoteEntry(name, url)
// Initialize container
await container.init(__webpack_share_scopes__.default)
// Get module factory
const factory = await container.get('./ProductsModule')
// Cache
this.loadedRemotes.set(name, factory)
return factory
}
private async loadRemoteEntry(name: string, url: string): Promise<any> {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = url
script.onload = () => {
// Remote is loaded, get container
const container = window[name]
resolve(container)
}
script.onerror = () => reject(new Error(`Failed to load remote: ${name}`))
document.head.appendChild(script)
})
}
}
Use in Routes
// Host App - Dynamic route loading
const routes: Routes = [
{
path: 'products',
loadChildren: async () => {
const loader = inject(DynamicRemoteLoader)
const config = await fetch('/api/remotes/products').then(r => r.json())
const factory = await loader.loadRemote(
'products',
config.url // Runtime URL
)
return factory()
}
}
]
Why This Works
✅ Runtime Configuration - URLs from API, not hardcoded
✅ Feature Flags - Load remotes conditionally
✅ A/B Testing - Load different versions
✅ Environment-Specific - Different URLs per environment
Strategy 2: Version Management
The Problem
Products uses Angular 15. Checkout uses Angular 16. How do they work together?
The Solution: Version Tolerance in Shared Dependencies
// Host App - webpack.config.js
shared: {
'@angular/core': {
singleton: true,
strictVersion: false, // Allow different versions
requiredVersion: '^15.0.0 || ^16.0.0', // Accept both
eager: false
},
'@angular/common': {
singleton: true,
strictVersion: false,
requiredVersion: '^15.0.0 || ^16.0.0',
eager: false
}
}
Products App (Angular 15)
// products-app/webpack.config.js
shared: {
'@angular/core': {
singleton: true,
strictVersion: false,
requiredVersion: '^15.0.0',
eager: false
}
}
Checkout App (Angular 16)
// checkout-app/webpack.config.js
shared: {
'@angular/core': {
singleton: true,
strictVersion: false,
requiredVersion: '^16.0.0',
eager: false
}
}
Why This Works
✅ Version Tolerance - Different versions can coexist
✅ Gradual Migration - Migrate apps one at a time
✅ No Coordination - Teams don't need to sync versions
Warning: This can cause issues. Use only when necessary.
Strategy 3: Conditional Remotes
The Problem
You want to load "Admin" remote only for admin users.
The Solution: Conditional Loading
// Host App - Conditional remote loading
const routes: Routes = [
{
path: 'admin',
loadChildren: async () => {
const authService = inject(AuthService)
const user = await authService.getUser().pipe(take(1)).toPromise()
if (user?.roles?.includes('admin')) {
// Load admin remote
return import('admin/AdminModule').then(m => m.AdminModule)
} else {
// Redirect or show error
throw new Error('Unauthorized')
}
},
canActivate: [AuthGuard]
}
]
Feature Flag Based
// Host App - Feature flag based loading
const routes: Routes = [
{
path: 'analytics',
loadChildren: async () => {
const featureFlags = inject(FeatureFlagService)
const isEnabled = await featureFlags.isEnabled('analytics').pipe(
take(1)
).toPromise()
if (isEnabled) {
return import('analytics/AnalyticsModule').then(m => m.AnalyticsModule)
} else {
return import('./fallback/analytics-fallback.module')
.then(m => m.AnalyticsFallbackModule)
}
}
}
]
Strategy 4: Runtime Configuration
The Problem
You want to change remote URLs without rebuilding the Host app.
The Solution: Configuration API
// Host App - Runtime config service
@Injectable({ providedIn: 'root' })
export class RemoteConfigService {
private config$ = new BehaviorSubject<RemoteConfig | null>(null)
constructor(private http: HttpClient) {
this.loadConfig()
}
private async loadConfig() {
const config = await this.http.get<RemoteConfig>('/api/remotes/config')
.pipe(take(1))
.toPromise()
this.config$.next(config)
}
getRemoteUrl(name: string): string | null {
const config = this.config$.value
return config?.remotes?.[name]?.url ?? null
}
getRemoteVersion(name: string): string | null {
const config = this.config$.value
return config?.remotes?.[name]?.version ?? null
}
}
// Host App - Use runtime config
const routes: Routes = [
{
path: 'products',
loadChildren: async () => {
const configService = inject(RemoteConfigService)
const url = configService.getRemoteUrl('products')
const version = configService.getRemoteVersion('products')
if (!url) {
throw new Error('Products remote not configured')
}
const fullUrl = `${url}/v${version}/remoteEntry.js`
return loadRemote('products', fullUrl)
.then(m => m.ProductsModule)
}
}
]
Configuration API Response
{
"remotes": {
"products": {
"url": "https://products.example.com",
"version": "1.2.3"
},
"checkout": {
"url": "https://checkout.example.com",
"version": "2.0.1"
}
}
}
Benefits:
- ✅ Change URLs without rebuild
- ✅ Version management
- ✅ Environment-specific configs
- ✅ A/B testing support
Strategy 5: Advanced Shared Dependency Strategies
The Problem
Some dependencies should be shared, some shouldn't. Some need different versions.
The Solution: Granular Sharing Rules
// Host App - Advanced sharing
shared: {
// Always share, must match version
'@angular/core': {
singleton: true,
strictVersion: true,
requiredVersion: '^15.0.0',
eager: false
},
// Share if available, allow different versions
'lodash': {
singleton: false, // Each app can have its own
strictVersion: false,
requiredVersion: '^4.0.0 || ^5.0.0',
eager: false
},
// Don't share, each app bundles its own
'@my-company/products-lib': {
singleton: false,
strictVersion: false,
requiredVersion: false, // Don't share
eager: false
},
// Eager load (load immediately)
'rxjs': {
singleton: true,
strictVersion: true,
requiredVersion: '^7.0.0',
eager: true // Load immediately
}
}
Sharing Rules Explained
- singleton: true - One instance shared across all apps
- singleton: false - Each app gets its own instance
- strictVersion: true - Versions must match exactly
- strictVersion: false - Versions can differ (within requiredVersion)
- eager: true - Load immediately (not lazy)
- eager: false - Load on demand (lazy)
Strategy 6: Custom Webpack Configuration
The Problem
You need custom webpack plugins or loaders for Module Federation.
The Solution: Custom Webpack Config
// Host App - webpack.config.js
const ModuleFederationPlugin = require('@module-federation/webpack')
const { merge } = require('webpack-merge')
module.exports = (config, options) => {
const mfConfig = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
products: 'products@http://localhost:4201/remoteEntry.js'
},
shared: {
'@angular/core': { singleton: true, strictVersion: true }
}
})
]
}
// Merge with Angular's webpack config
return merge(config, mfConfig)
}
Custom Loaders
// Add custom loaders
module.exports = (config, options) => {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack']
})
return config
}
Production Checklist
- [ ] Dynamic remote loading implemented
- [ ] Version management strategy defined
- [ ] Conditional loading for feature flags
- [ ] Runtime configuration API
- [ ] Advanced sharing rules configured
- [ ] Custom webpack config (if needed)
- [ ] Documentation for advanced configs
Key Takeaways
- Dynamic loading - Load remotes at runtime
- Version tolerance - Handle different versions carefully
- Conditional loading - Load based on conditions
- Runtime config - Change URLs without rebuild
- Granular sharing - Control what's shared
Remember: Advanced configurations give you power, but also complexity. Use only when needed.
The next module: Debugging real production issues.