Front-end Engineering Lab
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 rollback

Implementation

# 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.0

Implementation

// 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 deployments

Spotify

- Feature flags for everything
- Blue-green per service
- Version pinning for compatibility
- Gradual rollout over days

Amazon

- Independent MFE deployments
- Canary with automated rollback
- Regional rollouts
- Strict version compatibility

📚 Key Takeaways

  1. Deploy independently - Don't coordinate
  2. Use versioned URLs - Keep old versions available
  3. Canary new versions - Start with small % of users
  4. Monitor everything - Auto-rollback on errors
  5. Feature flags - Decouple deploy from release
  6. Version pinning - Ensure compatibility
  7. Test in production - Canary IS testing

Start simple (CDN + versions), add sophistication (canary, feature flags) as you scale.

On this page