Versioning Strategies for Remote Apps #

Independent deployment of micro-frontends requires robust versioning to prevent runtime crashes, dependency conflicts, and silent API drift. This guide details configuration patterns, runtime resolution strategies, and deployment workflows for safely managing remote app versions in a Module Federation ecosystem.

Key Implementation Objectives:


Setup/Config #

Establish baseline Module Federation configuration with explicit version constraints, shared dependency resolution, and remote exposure boundaries. Proper configuration at build time dictates how Webpack resolves shared modules and exposes remote entry points.

Implementation Steps:

// webpack.config.js (Host Application)
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
 // ... other config
 plugins: [
 new ModuleFederationPlugin({
 name: 'host_app',
 remotes: {
 // Build-time injection of remote version via CI environment
 remoteUI: `remoteUI@${process.env.REMOTE_VERSION || 'latest'}/remoteEntry.js`
 },
 shared: {
 // Core frameworks MUST be singletons with strict version enforcement
 react: { singleton: true, strictVersion: true, requiredVersion: '^18.2.0' },
 'react-dom': { singleton: true, strictVersion: true, requiredVersion: '^18.2.0' },
 // Internal UI libs can tolerate minor drift; disable strict enforcement
 '@shared/ui': { singleton: false, strictVersion: false, requiredVersion: '^1.0.0' }
 }
 })
 ]
};

Runtime vs Build-Time Implications: The remotes configuration is evaluated at build time. Injecting process.env.REMOTE_VERSION allows CI to pin a specific remote build during compilation, while the shared configuration dictates runtime dependency resolution. Setting strictVersion: true forces Webpack to throw a fatal error at runtime if the loaded remote provides an incompatible version, preventing silent context corruption.


Integration #

Implement host-side remote loading logic that dynamically resolves versions, handles fallbacks, and negotiates compatibility at runtime. Static remote paths limit deployment flexibility; dynamic resolution enables independent release cycles.

Implementation Steps:

// src/versionResolver.js
async function loadRemoteModule(remoteName, requestedVersion) {
 // Fetch centralized manifest at runtime (cached via CDN)
 const manifest = await fetch('/cdn/version-manifest.json').then(r => r.json());
 const resolvedVersion = manifest[remoteName]?.[requestedVersion] || manifest[remoteName]?.latest;
 
 if (!resolvedVersion) throw new Error(`No version found for ${remoteName}`);
 
 try {
 // Dynamic import triggers Module Federation remote loading at runtime
 return await import(`@${remoteName}/${resolvedVersion}/exposedModule`);
 } catch (err) {
 console.warn(`Failed to load v${resolvedVersion}, falling back to previous stable`);
 // Graceful degradation: load previously verified stable version
 return await import(`@${remoteName}/${manifest[remoteName].previousStable}/exposedModule`);
 }
}

Runtime vs Build-Time Implications: This pattern shifts version resolution entirely to runtime. The host bundle no longer contains hardcoded remote paths, allowing remotes to deploy independently. The manifest fetch occurs asynchronously before component hydration, ensuring the correct remoteEntry.js is fetched and evaluated before Webpack’s runtime attempts to satisfy shared dependencies.


Edge Cases #

Address version drift, partial upgrades, shared dependency conflicts, and fallback routing when version resolution fails. Micro-frontend ecosystems inevitably encounter mismatched deployment states.

Implementation Steps:

Debugging Workflow: When strictVersion triggers a mismatch, Webpack logs a Version mismatch error in the console. To diagnose:

  1. Check the remote’s package.json vs host’s shared.requiredVersion
  2. Verify CDN manifest points to the correct remoteEntry.js hash
  3. Use browser devtools to inspect __webpack_require__.m for loaded module versions
  4. If fallback chains fail, ensure the static placeholder component is bundled in the host to avoid blank screens during network degradation

Testing/Validation #

Validate version compatibility across deployment matrices, enforce API contracts, and automate regression detection for remote modules. Versioning strategies fail without automated verification.

Implementation Steps:

#!/usr/bin/env node
// scripts/validate-compatibility.js
const { execSync } = require('child_process');
const versions = ['1.4.0', '1.5.0', '2.0.0']; // Matrix of supported remote versions

versions.forEach(v => {
 console.log(`Testing host against remote v${v}...`);
 try {
 // Pass version to test runner to dynamically load remote entry
 execSync(`npm run test:integration -- --remote-version=${v}`, { stdio: 'inherit' });
 console.log(`✅ v${v} compatible`);
 } catch (e) {
 console.error(`❌ v${v} broke host contract`);
 process.exit(1); // Fail CI pipeline immediately
 }
});

Runtime vs Build-Time Implications: This script executes at build/CI time, simulating runtime loading conditions. By injecting --remote-version into the test runner, you replicate the exact dynamic import behavior that will occur in production. Failing fast here prevents incompatible remotes from ever reaching the staging manifest.


Deployment #

Execute canary releases, blue/green deployments, cache invalidation, and rollback procedures for versioned remote apps. Deployment strategy dictates how version manifests are updated and propagated.

Implementation Steps:

Rollback Procedure:

  1. Identify failing remote version in telemetry
  2. Update /cdn/version-manifest.json to point latest back to the previous stable hash
  3. Invalidate CDN edge cache for the manifest only (do not clear immutable remote assets)
  4. Hosts automatically resolve the new pointer on next page load or dynamic import, requiring zero host redeployment.

Common Pitfalls #

Issue Root Cause & Resolution
Shared dependency duplication causing multiple React instances Omitting singleton: true or misconfiguring requiredVersion ranges allows webpack to bundle separate framework copies, triggering context loss and hydration errors. Always enforce singletons for stateful UI frameworks.
CDN cache serving stale remoteEntry.js Without immutable caching and hash-based filenames, browsers cache outdated manifests, causing hosts to load mismatched module versions. Use content-addressed filenames (remoteEntry.[contenthash].js) and strict cache headers.
Silent API drift between minor versions Relying solely on semantic versioning without contract testing allows breaking prop or hook changes to slip through, crashing host components at runtime. Enforce TypeScript interface validation in CI.
Version pinning blocking security patches Hardcoding exact versions (1.2.3) instead of ranges (^1.2.3) prevents automatic patch adoption, increasing vulnerability exposure and maintenance overhead. Use caret ranges with automated patch dependency updates.

FAQ #

How do I prevent version conflicts when multiple hosts consume the same remote? Use a centralized version manifest and enforce strictVersion: true in shared configs. Hosts should resolve versions at runtime via the manifest rather than hardcoding remote paths. This ensures all hosts negotiate against the same compatibility baseline.

Can I run multiple versions of the same remote simultaneously? Yes, by exposing versioned entry points (e.g., /v1/remoteEntry.js, /v2/remoteEntry.js) and using dynamic imports. Ensure shared dependencies are properly namespaced or isolated to prevent runtime collisions. This pattern is essential for gradual migration during major version upgrades.

What happens if a remote version fails to load at runtime? Implement a fallback chain in your loader: attempt the requested version, fall back to the previous stable, and finally render a static placeholder or error boundary. Log telemetry for rapid incident response. Never allow unhandled promise rejections to crash the host shell.