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:
- Runtime version negotiation prevents host-remote incompatibility
- Strict dependency sharing avoids duplicate framework instances
- Contract-based versioning enables zero-downtime independent releases
- Understanding foundational Core Micro-Frontend Architecture & Tradeoffs is essential before implementing version locks
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:
- Configure
ModuleFederationPluginwithshareddependencies usingstrictVersion: trueandsingleton: truefor framework packages - Define
requiredVersionranges inpackage.jsonand mirror them exactly in the webpack configuration to prevent resolution mismatches - Isolate remote entry points to prevent cross-boundary leakage, aligning with Defining Application Boundaries
- Implement version manifest generation during CI builds to track exposed module hashes and dependency trees
// 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:
- Use dynamic
import()with versioned remote URLs (e.g.,/remote/v2.1.0/remoteEntry.js) - Build a version resolver that queries a central registry or CDN manifest before loading
- Decouple host routing from remote versions to reduce Managing Cross-Team Coupling
- Implement graceful degradation when requested remote versions are unavailable
// 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:
- Detect and resolve
strictVersionmismatches before hydration by wrapping remote loaders in error boundaries - Handle partial upgrades where host and remote share incompatible minor versions by implementing compatibility matrices
- Apply Semantic versioning for independently deployed UI modules to enforce contract stability across teams
- Implement fallback chains: latest stable -> previous minor -> cached static build
Debugging Workflow:
When strictVersion triggers a mismatch, Webpack logs a Version mismatch error in the console. To diagnose:
- Check the remote’s
package.jsonvs host’sshared.requiredVersion - Verify CDN manifest points to the correct
remoteEntry.jshash - Use browser devtools to inspect
__webpack_require__.mfor loaded module versions - 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:
- Run contract tests against multiple remote versions using snapshot assertions to verify exposed component props
- Validate shared dependency trees with
webpack-bundle-analyzerandnpm lsto detect duplicate framework instances - Automate Handling breaking changes in remote module APIs via CI pipeline gates
- Execute integration smoke tests against staging CDN endpoints before promotion to production
#!/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:
- Deploy remotes to versioned CDN paths with immutable caching headers (
Cache-Control: public, max-age=31536000, immutable) - Implement feature flags to toggle remote version exposure per tenant or user segment
- Automate manifest updates only after health checks pass on staging
- Configure instant rollback by reverting manifest pointers without rebuilding hosts
Rollback Procedure:
- Identify failing remote version in telemetry
- Update
/cdn/version-manifest.jsonto pointlatestback to the previous stable hash - Invalidate CDN edge cache for the manifest only (do not clear immutable remote assets)
- 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.