Module 23: Migration from Monolith to Micro Frontends (Senior/Enterprise)
Chapter 23 • Advanced
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
- Zero Downtime - Production must keep running
- Continuous Development - Teams can't stop working
- Gradual Migration - Can't migrate everything at once
- Risk Management - Each step must be safe
- 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.
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
// 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
// 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.
- Products - No dependencies → Migrate first
- Admin - No dependencies → Migrate second
- Checkout - Depends on Products → Migrate last
Phase 2: Extract First Domain (Products)
Step 1: Create Products Micro Frontend
// 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
// 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
// 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
// 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
// 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
// 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
// Monitor for 1-2 weeks
// - Error rates
// - Performance metrics
// - User feedback
// - Team feedback
Step 2: Remove Old Code
// 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
// 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
- Create Admin micro frontend
- Deploy Admin app
- Update monolith to load Admin remote
- Use feature flag
- Validate
- Remove old code
Phase 6: Extract Final Domain (Checkout)
Special Consideration: Dependencies
Checkout depends on Products. Both are now remotes.
Solution: Shared Services
// 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
// 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
// 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
// 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
// Deploy to: https://app.example.com
// This becomes the new entry point
Step 4: Redirect Monolith
// 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
- Strangler Pattern - Gradual replacement
- Coexistence - Old and new work together
- Feature Flags - Safe rollouts
- Independent Domains First - Reduce dependencies
- 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.
Related Tutorials
Previous: Module 22: Debugging Real Production Issues (Senior/Enterprise)
Learn about module 22: debugging real production issues (senior/enterprise)
Next: Module 24: Styling & Design System Integration (Senior/Enterprise)
Continue with module 24: styling & design system integration (senior/enterprise)