23

Module 23: Migration from Monolith to Micro Frontends (Senior/Enterprise)

Chapter 23 • Advanced

50 min

Migration from Monolith to Micro Frontends (Senior/Enterprise)

The Real Problem

Scenario: You have a 5-year-old Angular monolith. 50 developers. 500 components. Production can't stop. Teams are blocked.

The Question: How do I migrate a large Angular monolith to micro frontends without breaking production or stopping development?

This is the Strangler Pattern.


The Production Reality

Migration Constraints

  1. Zero Downtime - Production must keep running
  2. Continuous Development - Teams can't stop working
  3. Gradual Migration - Can't migrate everything at once
  4. Risk Management - Each step must be safe
  5. Team Coordination - Multiple teams involved

The Question: How do we migrate safely?


Strategy: The Strangler Pattern

The Concept

Gradually replace parts of the monolith with micro frontends, one domain at a time.

code
Monolith (Old)
    ↓
Coexistence (Old + New)
    ↓
Micro Frontends (New)

Key Principle: Old and new coexist. Old is gradually "strangled" by new.


Phase 1: Identify Boundaries

Step 1: Map Business Domains

typescript.js
// Analyze monolith structure
const domains = {
  products: {
    components: ['ProductList', 'ProductDetail', 'ProductSearch'],
    routes: ['/products', '/products/:id'],
    services: ['ProductService', 'ProductApiService'],
    team: 'Products Team'
  },
  checkout: {
    components: ['Cart', 'Checkout', 'Payment'],
    routes: ['/cart', '/checkout', '/payment'],
    services: ['CartService', 'PaymentService'],
    team: 'Checkout Team'
  },
  admin: {
    components: ['AdminDashboard', 'UserManagement'],
    routes: ['/admin', '/admin/users'],
    services: ['AdminService'],
    team: 'Admin Team'
  }
}

Step 2: Identify Dependencies

typescript.js
// Map dependencies between domains
const dependencies = {
  products: {
    dependsOn: [], // No dependencies
    usedBy: ['checkout'] // Checkout uses products
  },
  checkout: {
    dependsOn: ['products'], // Depends on products
    usedBy: []
  },
  admin: {
    dependsOn: [],
    usedBy: []
  }
}

Step 3: Choose Migration Order

Rule: Migrate independent domains first.

  1. Products - No dependencies → Migrate first
  2. Admin - No dependencies → Migrate second
  3. Checkout - Depends on Products → Migrate last

Phase 2: Extract First Domain (Products)

Step 1: Create Products Micro Frontend

typescript.js
// Create new Products app
npx @angular/cli@20 new products-app

// Copy components from monolith
// - ProductListComponent
// - ProductDetailComponent
// - ProductSearchComponent

// Copy services
// - ProductService
// - ProductApiService

Step 2: Configure Module Federation

typescript.js
// products-app/webpack.config.js
new ModuleFederationPlugin({
  name: 'products',
  filename: 'remoteEntry.js',
  exposes: {
    './ProductsModule': './src/app/products/products.module.ts'
  },
  shared: {
    '@angular/core': { singleton: true, strictVersion: true },
    '@angular/common': { singleton: true, strictVersion: true }
  }
})

Step 3: Deploy Products App

typescript.js
// Deploy to: https://products.example.com
// Test standalone: https://products.example.com/products

Phase 3: Coexistence (Old + New)

Step 1: Update Monolith to Load Remote

typescript.js
// monolith/webpack.config.js
new ModuleFederationPlugin({
  name: 'monolith',
  remotes: {
    products: 'products@https://products.example.com/remoteEntry.js'
  },
  shared: {
    '@angular/core': { singleton: true, strictVersion: true }
  }
})

Step 2: Replace Route in Monolith

typescript.js
// monolith/src/app/app-routing.module.ts
const routes: Routes = [
  // Old route (commented out, not deleted)
  // {
  //   path: 'products',
  //   loadChildren: () => import('./products/products.module')
  //     .then(m => m.ProductsModule)
  // },
  
  // New route (loads remote)
  {
    path: 'products',
    loadChildren: () => import('products/ProductsModule')
      .then(m => m.ProductsModule)
  },
  
  // Other routes unchanged
  {
    path: 'checkout',
    loadChildren: () => import('./checkout/checkout.module')
      .then(m => m.CheckoutModule)
  }
]

Step 3: Feature Flag for Safety

typescript.js
// monolith/src/app/app-routing.module.ts
const routes: Routes = [
  {
    path: 'products',
    loadChildren: () => {
      const useRemote = environment.featureFlags.useProductsRemote
      
      if (useRemote) {
        // Load remote (new)
        return import('products/ProductsModule')
          .then(m => m.ProductsModule)
          .catch(() => {
            // Fallback to monolith if remote fails
            console.warn('Remote failed, using monolith')
            return import('./products/products.module')
              .then(m => m.ProductsModule)
          })
      } else {
        // Load from monolith (old)
        return import('./products/products.module')
          .then(m => m.ProductsModule)
      }
    }
  }
]

Why This Works

Zero Risk - Can rollback instantly (feature flag)

Gradual Rollout - Enable for 10% of users first

Fallback - If remote fails, use monolith

No Downtime - Old code still works


Phase 4: Remove Old Code (After Validation)

Step 1: Validate Migration

typescript.js
// Monitor for 1-2 weeks
// - Error rates
// - Performance metrics
// - User feedback
// - Team feedback

Step 2: Remove Old Code

typescript.js
// Once validated, remove old code
// monolith/src/app/products/  ← Delete this folder

// Update routes (remove fallback)
const routes: Routes = [
  {
    path: 'products',
    loadChildren: () => import('products/ProductsModule')
      .then(m => m.ProductsModule)
  }
]

Step 3: Update Dependencies

typescript.js
// Checkout (still in monolith) now uses Products remote
// Update Checkout to use Products remote APIs
// Or keep using shared services (if compatible)

Phase 5: Extract Second Domain (Admin)

Repeat Process

  1. Create Admin micro frontend
  2. Deploy Admin app
  3. Update monolith to load Admin remote
  4. Use feature flag
  5. Validate
  6. Remove old code

Phase 6: Extract Final Domain (Checkout)

Special Consideration: Dependencies

Checkout depends on Products. Both are now remotes.

Solution: Shared Services

typescript.js
// Host App - Provides shared services
@Injectable({ providedIn: 'root' })
export class CartService {
  // Shared cart logic
}

// Products App - Uses shared service
export class ProductsComponent {
  constructor(private cart: CartService) {}
  
  addToCart(product: Product) {
    this.cart.addItem(product)
  }
}

// Checkout App - Uses shared service
export class CheckoutComponent {
  constructor(private cart: CartService) {}
  
  getItems() {
    return this.cart.getItems()
  }
}

Or: Event-Based Communication

typescript.js
// Products emits event
this.eventBus.emit({
  event: 'cart:item-added',
  data: { product }
})

// Checkout listens
this.eventBus.on('cart:item-added').subscribe(event => {
  this.cart.addItem(event.data.product)
})

Phase 7: Create Host App

Step 1: Extract Shell

typescript.js
// Create Host app
npx @angular/cli@20 new host-app

// Extract from monolith:
// - Shell component
// - Layout components
// - Navigation
// - Auth service
// - Shared services

Step 2: Configure Host

typescript.js
// host-app/webpack.config.js
new ModuleFederationPlugin({
  name: 'host',
  remotes: {
    products: 'products@https://products.example.com/remoteEntry.js',
    checkout: 'checkout@https://checkout.example.com/remoteEntry.js',
    admin: 'admin@https://admin.example.com/remoteEntry.js'
  },
  shared: {
    '@angular/core': { singleton: true, strictVersion: true }
  }
})

Step 3: Deploy Host

typescript.js
// Deploy to: https://app.example.com
// This becomes the new entry point

Step 4: Redirect Monolith

typescript.js
// monolith/src/app/app.component.ts
ngOnInit() {
  // Redirect to new Host
  window.location.href = 'https://app.example.com' + this.router.url
}

Migration Timeline

Month 1-2: Preparation

  • Identify boundaries
  • Create Products micro frontend
  • Deploy and test

Month 3: Products Migration

  • Coexistence phase
  • Feature flag rollout
  • Validation

Month 4: Admin Migration

  • Extract Admin
  • Coexistence
  • Validation

Month 5: Checkout Migration

  • Extract Checkout
  • Handle dependencies
  • Validation

Month 6: Host Creation

  • Create Host app
  • Migrate shell
  • Final cutover

Total: 6 months for complete migration


Risk Mitigation

Risk 1: Remote Fails

Mitigation: Feature flags + fallback to monolith

Risk 2: Version Conflicts

Mitigation: Strict versioning in shared dependencies

Risk 3: Team Coordination

Mitigation: Clear boundaries, independent deployment

Risk 4: Performance Degradation

Mitigation: Monitor metrics, optimize loading


Production Checklist

  • [ ] Boundaries identified
  • [ ] Migration order defined
  • [ ] First domain extracted
  • [ ] Coexistence implemented
  • [ ] Feature flags configured
  • [ ] Monitoring in place
  • [ ] Rollback plan ready
  • [ ] Team communication plan

Key Takeaways

  1. Strangler Pattern - Gradual replacement
  2. Coexistence - Old and new work together
  3. Feature Flags - Safe rollouts
  4. Independent Domains First - Reduce dependencies
  5. Validate Before Removing - Don't rush

Remember: Migration is a marathon, not a sprint. Safety over speed.

The next module: Styling and design system integration.