PatternsMicrofrontends
Deployment Strategies
Best practices for deploying microfrontends independently and safely
Independent deployments are a key benefit of microfrontends. This guide covers proven deployment patterns used by large-scale applications.
🎯 Goals
What we want:
✅ Deploy MFEs independently
✅ No coordination between teams
✅ Zero downtime deployments
✅ Easy rollbacks
✅ Version compatibility📦 Pattern 1: Independent CDN Deployments
Each MFE deploys to its own CDN path.
Structure
CDN Layout:
https://cdn.example.com/
├── header/
│ ├── v1.0.0/
│ │ ├── remoteEntry.js
│ │ ├── main.js
│ │ └── styles.css
│ ├── v1.1.0/
│ └── latest/ → v1.1.0
├── products/
│ ├── v2.0.0/
│ └── latest/
└── checkout/
├── v1.5.0/
└── latest/CI/CD Pipeline
# .github/workflows/deploy-header.yml
name: Deploy Header MFE
on:
push:
paths:
- 'packages/header/**'
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: '18'
- name: Install dependencies
run: npm ci
working-directory: ./packages/header
- name: Build
run: npm run build
working-directory: ./packages/header
env:
VERSION: ${{ github.sha }}
- name: Deploy to CDN
run: |
VERSION=${{ github.sha }}
aws s3 sync dist/ s3://cdn.example.com/header/$VERSION/
aws s3 sync dist/ s3://cdn.example.com/header/latest/
working-directory: ./packages/header
- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_ID }} \
--paths "/header/latest/*"Shell Configuration
// shell/config.ts
const MFE_URLS = {
header: process.env.HEADER_URL || 'https://cdn.example.com/header/latest/remoteEntry.js',
products: process.env.PRODUCTS_URL || 'https://cdn.example.com/products/latest/remoteEntry.js',
checkout: process.env.CHECKOUT_URL || 'https://cdn.example.com/checkout/latest/remoteEntry.js',
};
// webpack.config.js (Shell)
new ModuleFederationPlugin({
name: 'shell',
remotes: {
header: `header@${MFE_URLS.header}`,
products: `products@${MFE_URLS.products}`,
checkout: `checkout@${MFE_URLS.checkout}`,
},
});🔄 Pattern 2: Blue-Green Deployments
Run two versions simultaneously, switch traffic instantly.
Before deployment:
All traffic → Blue (v1.0.0)
Green (v1.1.0) idle
During deployment:
Deploy to Green (v1.1.0)
Test Green
Ready? Switch DNS/Load Balancer
After deployment:
All traffic → Green (v1.1.0)
Blue (v1.0.0) kept for rollbackImplementation
# Terraform example
resource "aws_lb_target_group" "header_blue" {
name = "header-blue"
port = 80
# ... config
}
resource "aws_lb_target_group" "header_green" {
name = "header-green"
port = 80
# ... config
}
resource "aws_lb_listener_rule" "header" {
listener_arn = aws_lb_listener.main.arn
action {
type = "forward"
target_group_arn = var.active_target == "blue"
? aws_lb_target_group.header_blue.arn
: aws_lb_target_group.header_green.arn
}
}Deployment Script
#!/bin/bash
CURRENT=$(get_active_target) # "blue" or "green"
NEW=$([ "$CURRENT" == "blue" ] && echo "green" || echo "blue")
echo "Current: $CURRENT, Deploying to: $NEW"
# Deploy to inactive target
deploy_to_target $NEW
# Run smoke tests
run_smoke_tests $NEW
# Switch traffic
switch_traffic $NEW
echo "Deployment complete. $NEW is now active."🎯 Pattern 3: Canary Deployments
Gradually roll out new version to subset of users.
Phase 1: 5% traffic → v2.0.0
95% traffic → v1.0.0
Phase 2: 25% traffic → v2.0.0
75% traffic → v1.0.0
Phase 3: 50% traffic → v2.0.0
50% traffic → v1.0.0
Phase 4: 100% traffic → v2.0.0Implementation
// Shell config with canary logic
function getHeaderURL(): string {
const userId = getCurrentUserId();
const canaryPercentage = 10; // 10% of users
const isCanary = (hashCode(userId) % 100) < canaryPercentage;
if (isCanary) {
return 'https://cdn.example.com/header/v2.0.0-canary/remoteEntry.js';
}
return 'https://cdn.example.com/header/v1.0.0/remoteEntry.js';
}
function hashCode(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash = hash & hash;
}
return Math.abs(hash);
}Progressive Rollout
// Automated canary with metrics
class CanaryDeployment {
async deploy(mfeName: string, newVersion: string) {
const phases = [5, 25, 50, 100]; // Percentages
for (const percentage of phases) {
console.log(`Rolling out to ${percentage}% of users...`);
// Update routing to send % to new version
await updateRouting(mfeName, newVersion, percentage);
// Wait and monitor
await sleep(5 * 60 * 1000); // 5 minutes
// Check metrics
const metrics = await getMetrics(mfeName, newVersion);
if (metrics.errorRate > 0.01) {
console.error('Error rate too high! Rolling back...');
await rollback(mfeName);
throw new Error('Canary failed');
}
if (metrics.latencyP99 > 1000) {
console.error('Latency too high! Rolling back...');
await rollback(mfeName);
throw new Error('Canary failed');
}
}
console.log('Canary successful!');
}
}⏪ Pattern 4: Instant Rollback
Quick revert to previous version.
// Version manifest
interface VersionManifest {
mfeName: string;
versions: {
current: string;
previous: string;
stable: string;
};
}
// Store manifest in database/S3
const manifest: VersionManifest = {
mfeName: 'header',
versions: {
current: 'v1.1.0',
previous: 'v1.0.0',
stable: 'v1.0.0'
}
};
// Rollback function
async function rollback(mfeName: string) {
const manifest = await getManifest(mfeName);
// Point "latest" to previous version
await updateCDN(mfeName, 'latest', manifest.versions.previous);
// Update manifest
manifest.versions.current = manifest.versions.previous;
await saveManifest(manifest);
// Invalidate CDN cache
await invalidateCache(mfeName);
console.log(`Rolled back ${mfeName} to ${manifest.versions.previous}`);
}Automated Rollback
// Monitor and auto-rollback
class HealthMonitor {
private errorThreshold = 0.05; // 5% error rate
async monitor(mfeName: string) {
setInterval(async () => {
const metrics = await getMetrics(mfeName);
if (metrics.errorRate > this.errorThreshold) {
console.error(`${mfeName} error rate: ${metrics.errorRate}. Rolling back...`);
await rollback(mfeName);
await alertTeam(mfeName, 'Auto-rolled back due to high error rate');
}
}, 60000); // Check every minute
}
}📍 Pattern 5: Feature Flags
Deploy code without activating features.
// Feature flag service
interface FeatureFlags {
newCheckoutFlow: boolean;
betaHeader: boolean;
improvedSearch: boolean;
}
class FeatureFlagService {
async getFlags(userId: string): Promise<FeatureFlags> {
// Fetch from service (LaunchDarkly, Split.io, etc)
const response = await fetch(`/api/feature-flags?userId=${userId}`);
return response.json();
}
async isEnabled(flag: keyof FeatureFlags, userId: string): Promise<boolean> {
const flags = await this.getFlags(userId);
return flags[flag] || false;
}
}
export const featureFlags = new FeatureFlagService();Usage in MFE
// Header MFE
import { featureFlags } from '@company/shared';
export function Header() {
const [showBeta, setShowBeta] = useState(false);
useEffect(() => {
featureFlags.isEnabled('betaHeader', userId).then(setShowBeta);
}, [userId]);
if (showBeta) {
return <BetaHeader />;
}
return <RegularHeader />;
}Gradual Rollout with Flags
// Backend API
app.get('/api/feature-flags', (req, res) => {
const userId = req.query.userId;
const userHash = hashCode(userId) % 100;
res.json({
newCheckoutFlow: userHash < 20, // 20% of users
betaHeader: userHash < 10, // 10% of users
improvedSearch: true, // Everyone
});
});🔒 Pattern 6: Version Pinning
Lock specific MFE versions together for compatibility.
// version-manifest.json
{
"compatibility": {
"2024-01": {
"shell": "v3.0.0",
"header": "v1.5.0",
"products": "v2.1.0",
"checkout": "v1.8.0"
},
"2024-02": {
"shell": "v3.1.0",
"header": "v1.6.0",
"products": "v2.2.0",
"checkout": "v1.9.0"
}
}
}
// Shell loads compatible versions
async function loadMFEVersions() {
const manifest = await fetch('/version-manifest.json').then(r => r.json());
const targetVersion = '2024-01'; // Or get from config
const versions = manifest.compatibility[targetVersion];
return {
header: `https://cdn.example.com/header/${versions.header}/remoteEntry.js`,
products: `https://cdn.example.com/products/${versions.products}/remoteEntry.js`,
checkout: `https://cdn.example.com/checkout/${versions.checkout}/remoteEntry.js`,
};
}📊 Deployment Metrics
Track deployment success.
interface DeploymentMetrics {
mfeName: string;
version: string;
deployedAt: number;
metrics: {
errorRate: number;
latencyP50: number;
latencyP99: number;
successRate: number;
};
}
class DeploymentTracker {
async trackDeployment(deployment: DeploymentMetrics) {
// Send to monitoring service
await analytics.track('deployment', deployment);
// Compare with baseline
const baseline = await this.getBaseline(deployment.mfeName);
if (deployment.metrics.errorRate > baseline.errorRate * 1.5) {
await alertTeam(`${deployment.mfeName} error rate increased by 50%`);
}
}
async getBaseline(mfeName: string) {
// Get metrics from last 7 days
const metrics = await this.getHistoricalMetrics(mfeName, 7);
return this.calculateAverage(metrics);
}
}🏢 Real-World Examples
Netflix
- Canary deployments (1%, 10%, 50%, 100%)
- Automated rollback on error spike
- A/B testing integrated
- Multi-region deploymentsSpotify
- Feature flags for everything
- Blue-green per service
- Version pinning for compatibility
- Gradual rollout over daysAmazon
- Independent MFE deployments
- Canary with automated rollback
- Regional rollouts
- Strict version compatibility📚 Key Takeaways
- Deploy independently - Don't coordinate
- Use versioned URLs - Keep old versions available
- Canary new versions - Start with small % of users
- Monitor everything - Auto-rollback on errors
- Feature flags - Decouple deploy from release
- Version pinning - Ensure compatibility
- Test in production - Canary IS testing
Start simple (CDN + versions), add sophistication (canary, feature flags) as you scale.