Skip to content

Federation Common Issues & Trade-offs

Module Federation: Common Issues & Architectural Trade-offs

Section titled “Module Federation: Common Issues & Architectural Trade-offs”

Adopting a microfrontend (MFE) architecture using Webpack Module Federation or Native Federation is a major architectural commitment. While it resolves significant scaling limitations for multi-team workforces, it introduces fresh, complex categories of runtime faults, dependency arbitration issues, and network constraints.

This guide explores when Module Federation serves as an architectural savior, when it behaves as an anti-pattern, and provides a deep-dive troubleshooting handbook to address the most common runtime issues in production environments.


1. Architectural Trade-offs: Savior vs. Anti-pattern

Section titled “1. Architectural Trade-offs: Savior vs. Anti-pattern”

Before adopting Module Federation, organizations must perform a thorough cost-benefit analysis. Building an application over a distributed architecture is significantly harder than managing a cohesive monolithic codebase.

When Module Federation is a Savior (The Scaling Solution)

Section titled “When Module Federation is a Savior (The Scaling Solution)”

At a certain organizational size, traditional single-page application (SPA) architectures encounter severe developer velocity bottlenecks. Srijan Singh highlights the core pain points that Module Federation solves:

  • Continuous PR Blockages: In large monoliths, dozens of developers merge code into the same branch daily. This causes constant merge conflicts, rebuild queues, and release coordination bottlenecks. MFEs isolate domains so teams merge and deploy entirely within their own repositories.
  • Massive Build Times: If a simple text change requires running a CI/CD pipeline that compiles a 500,000-line codebase (often taking 30–45 minutes), team feedback loops stall. MFEs break compilation into micro-builds, reducing individual team CI steps to seconds or a few minutes.
  • Divergent Copy-and-Paste Components: When separate projects copy code from each other, subtle forks emerge, leading to inconsistent look-and-feel and duplicate efforts. Module Federation allows teams to publish and dynamically share widgets at runtime without publishing new NPM packages.
  • Single-Point-of-Failure Deployments: If a single bug in a low-priority route can crash the entire application for all users during boot, system availability is fragile. Module Federation isolates remotes; if a remote route crashes or fails to load, the host container can gracefully catch the error and keep other sections operational.

When Module Federation is an Anti-pattern (The Over-engineering Trap)

Section titled “When Module Federation is an Anti-pattern (The Over-engineering Trap)”

Federation is not a general best practice for all web projects. It acts as an anti-pattern in the following scenarios:

  • Small Teams (Under 20–30 Developers): If a single team or a small group of highly coordinated developers manages the application, introducing Module Federation adds massive operational overhead (CI configurations, dynamic routes, complex local mock setups) with no clear benefit.
  • Rapidly Evolving Shared Core Models: If the host and remotes constantly undergo coupled changes to their core contracts or shared API schemas, the team must coordinate releases synchronously anyway. This defeats the primary purpose of independent deployability and creates fragile runtime dependency errors.
  • Duplicate Dependency Bloating: Without strict sharing rules, each remote might bundle its own copy of heavy libraries (such as Angular core, Material, RxJS, or charting engines). This results in users downloading multiple megabytes of duplicate Javascript, severely harming Core Web Vitals and initial loading speeds.

2. Critical Runtime Troubleshooting Handbook

Section titled “2. Critical Runtime Troubleshooting Handbook”

Distributed applications require runtime coordination. Below are the most common Module Federation issues and concrete patterns to solve them.

Issue 1: “Shared module is not available for eager consumption”

Section titled “Issue 1: “Shared module is not available for eager consumption””

This is the most notorious error in Webpack Module Federation. It occurs when a bundle tries to execute synchronous code that relies on a shared library (such as @angular/core) before Module Federation has finished negotiating dependency versions with other remotes.

Uncaught Error: Shared module is not available for eager consumption
at Object.get (index.js:12)
at __webpack_require__.m (main.js:25)

Webpack must boot asynchronously to download the dependency maps and select the compatible shared singleton libraries. If the initial entry point executes synchronously, negotiations cannot occur.

To resolve this, split your startup sequence. Move the startup logic into a file called src/bootstrap.ts, and let your main entry file src/main.ts import it dynamically.

  1. Create or verify your src/bootstrap.ts:
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
  1. Rewrite your main entry point src/main.ts to utilize a dynamic import:
import('./bootstrap').catch((err) => console.error(err));

Make sure that angular.json refers to src/main.ts as the main file, not src/bootstrap.ts.


Issue 2: Version Mismatch & Singleton Conflicts

Section titled “Issue 2: Version Mismatch & Singleton Conflicts”

If the Host expects version 18.2.0 of a library but a loaded remote demands version 17.1.0 of the same library, conflicts arise. If the library (such as RxJS, NgRx, or Angular Core) maintains stateful singletons, loading it twice will result in runtime errors (e.g. rxjs operators failing to cast types, or missing service injection tokens).

The Solution: Sharing Policy in webpack.config.js

Section titled “The Solution: Sharing Policy in webpack.config.js”

Configure explicit sharing policies inside webpack.config.js under the shared property:

const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
shared: {
'@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'@angular/common': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'@angular/router': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'rxjs': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
},
});
  • singleton: true: Ensures only one instance of the module is loaded in the entire client session.
  • strictVersion: true: Forces the host/remote to fail fast or print explicit warnings if a remote requests an incompatible package version, avoiding silent and hard-to-debug runtime failures.
  • requiredVersion: 'auto': Looks at package.json to automatically determine compatible semver ranges.

Issue 3: CORS and Preflight (OPTIONS) Failures

Section titled “Issue 3: CORS and Preflight (OPTIONS) Failures”

Because remotes run on distinct servers or ports during local development or production, hosts must fetch their remote entry files and assets across origins. This triggers Cross-Origin Resource Sharing (CORS) security policies in the browser.

Access to script at 'http://localhost:4201/remoteEntry.js' from origin 'http://localhost:4200'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present.

Configure the web server hosting the remote files (e.g., Nginx, Apache, or local Webpack dev server) to attach appropriate headers.

For local development using proxy.conf.js or Webpack dev server config:

devServer: {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
"Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization"
}
}

In production, ensure your Content Delivery Network (CDN) or cloud storage bucket hosting remote bundles returns Access-Control-Allow-Origin: * or explicitly allows the host domain.


Issue 4: Dynamic Remote Loading Failures (CORS/Network Disconnects)

Section titled “Issue 4: Dynamic Remote Loading Failures (CORS/Network Disconnects)”

When loading remotes dynamically via code, network drops or deployment sequences can temporarily take down a remote bundle. A direct, unhandled loadRemoteModule call will crash the current routing transition.

The Solution: Resilient Route Error Handling

Section titled “The Solution: Resilient Route Error Handling”

Wrap dynamic component/module imports in standard try-catch blocks or build defensive loading utilities:

import { Route } from '@angular/router';
import { loadRemoteModule } from '@angular-architects/module-federation';
export const routes: Route[] = [
{
path: 'features-remote',
loadComponent: () =>
loadRemoteModule({
type: 'manifest',
remoteName: 'recursos-remote',
exposedModule: './Component'
})
.then((m) => m.FeatureComponent)
.catch((err) => {
console.error('Failed to load Microfrontend resources dynamically:', err);
return import('@shared/components/mfe-error.component')
.then((m) => m.MfeErrorComponent);
})
}
];

Issue 5: Local Mocking, Proxying, and Isolation Constraints

Section titled “Issue 5: Local Mocking, Proxying, and Isolation Constraints”

When developing microfrontends locally, teams face restrictions related to localized cross-origin requests, environment boundaries, mock servers, and global browser-error interception.

During local development, a proxy configuration block proxy.conf.js is required to route browser requests safely, bypassing operating systems’ cross-origin limitations.

To start the application pointing to localized mock ports:

Terminal window
# Starts the remote development server against local mocks in ams-front:
npm run start:local:mocks
# Runs the mock server and the application dev server together:
npm run start:local:mocks:full

Our proxy leverages cross-env to set environmental variables uniformly across macOS, Linux, and Windows to map endpoints safely:

// Example targets inside proxy.conf.js
const targets = {
'local-mocks': 'http://localhost:1080',
'dev': 'https://ed1bssaio01v.vsdepi.local:8243'
};

We maintain a mock API directory test/mocks/ams.json. To secure smooth operations:

  1. Always start the mock server under port 1080 (or the specific port mapped by your target remote/host):
    Terminal window
    npm run mocks # Starts @mockoon/cli mock server in localhost:1080
  2. Restrict route assets to relative pathways within components, or resolve them using the remote’s dynamic base URL. Avoid absolute routes (e.g. /assets/icon.svg) because the browser will attempt to load the asset from the Host’s domain name, resulting in 404 errors.
import { inject, Injectable } from '@angular/core';
import { REMOTE_BASE_URL } from './tokens';
@Injectable()
export class AssetService {
private baseUrl = inject(REMOTE_BASE_URL);
getAssetUrl(path: string): string {
return `${this.baseUrl}/${path}`;
}
}

If your remote MFE runs embedded within a larger Host Shell, the shell’s global interceptors (like ErrorHandlerInterceptor) will capture HTTP failures and display generic global error modals.

To bypass this behavior and allow your remote MFE’s facade to display context-specific error messages:

  • Insert the custom bypass header X-Skip-Error-Modal during request creation:
import { HttpHeaders, HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ResourceDetailsRepository {
constructor(private http: HttpClient) {}
fetchDetailsSilently(id: string) {
const headers = new HttpHeaders().set('X-Skip-Error-Modal', 'true');
return this.http.get(`/v1/resources/${id}`, { headers });
}
}

Rather than spinning up the entire Host Shell to test components under development, use an isolated playground. The custom routing file ams.routes.ts configures a visual playground route (/playground) available solely in development environments.

  1. Navigate to src/app/@core/components/playground/playground.component.ts (this directory is ignored in Git to allow manual additions).
  2. Render your remote components inside playground.component.html in isolation.
  3. Access of your component at http://localhost:4201/playground with zero host container dependencies.

Angular’s classic change detection relies on zone.js. In a Module Federation configuration, loading zone.js multiple times (once for host, and once in each dynamic bundle remote) degrades performance, causes state synchronization faults, and leads to infinite change detection cycles.

Ensure zone.js is loaded exactly once in the host container’s entry points.

  1. Ensure zone.js is declared in the Host’s src/index.html or src/main.ts under scripts/polyfills.
  2. In the remote MFEs, exclude zone.js from being processed or loaded. Within modern applications, you should prefer migrating your remote applications to OnPush change detection and eventually a zoneless change detection loop.

To opt out of Zone.js entirely at the bootstrap level of a remote when running under the host shell, configure zoneless based on your Angular framework version:

In Angular 19 and superior versions, zoneless is stable. Use the standard provider:

import { provideZonelessChangeDetection } from '@angular/core';
export const appConfig = {
providers: [
provideZonelessChangeDetection()
]
};

In Angular 18, zoneless is available under an experimental flag:

import { provideExperimentalZonelessChangeDetection } from '@angular/core';
export const appConfig = {
providers: [
provideExperimentalZonelessChangeDetection(),
]
};
C. Angular 17 and Below (Previous Versions)
Section titled “C. Angular 17 and Below (Previous Versions)”

For legacy applications where modern providers are not available, you must disable Zone.js in the bootstrap configuration options of your remote’s src/main.ts by passing 'noop' to ngZone:

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, {
...appConfig,
ngZone: 'noop'
}).catch((err) => console.error(err));

To deepen your understanding, consult these expert articles and community-validated resources:

  • Vasanthan K - Cross-App Communication in Micro Frontends: Detailed guide on establishing secure event buses, custom browser listeners, and shared global states. Cross-App Communication Pattern guide
  • Srijan Singh - Mastering Module Federation Part 1: Explains team constraints, bottlenecks of monolithic structures, and deployment autonomy trade-offs. Mastering Module Federation
  • Manfred Steyer - Module Federation in Angular: Comprehensive implementation techniques, dynamically-registered manifests, and compilation-agnostic import maps. Angular Architects Blog
  • Egor Malyshev - Microfrontends Architecture Guide: A complete study of client-side runtime dependency orchestration, security layers, and shared state across distributed UI packages.
  • Corporate Application Management Suite (ams-front README): Internal guidelines on setting up Mockoon loaders, dynamic target proxying, isolated playgounds, and the X-Skip-Error-Modal header.
  • Corporate Portal Container Shell (cosmos-front README): System details on setting up MSAL authentication, role interceptors, custom RouteReusableStrategy, and shared design layout variables.

This guide was generated in Spanish with English codebases for the Frontend Guild of Verisure. Ensure all changes to Webpack configs are peer-reviewed before being merged.