21

Module 21: Advanced Module Federation Configurations (Senior/Enterprise)

Chapter 21 • Advanced

40 min

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

  1. Dynamic Remote Loading - Load remotes at runtime, not build time
  2. Version Management - Handle different Angular versions
  3. Conditional Remotes - Load remotes based on feature flags
  4. Runtime Configuration - Change remote URLs without rebuild
  5. 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

typescript.js
// 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

typescript.js
// 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

typescript.js
// 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)

typescript.js
// products-app/webpack.config.js
shared: {
  '@angular/core': {
    singleton: true,
    strictVersion: false,
    requiredVersion: '^15.0.0',
    eager: false
  }
}

Checkout App (Angular 16)

typescript.js
// 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

typescript.js
// 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

typescript.js
// 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

typescript.js
// 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
  }
}
typescript.js
// 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

json.js
{
  "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

typescript.js
// 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

typescript.js
// 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

typescript.js
// 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

  1. Dynamic loading - Load remotes at runtime
  2. Version tolerance - Handle different versions carefully
  3. Conditional loading - Load based on conditions
  4. Runtime config - Change URLs without rebuild
  5. 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.