Migrating a Frontend Monolith to Micro Frontends
Learn how to migrate a frontend monolith to micro frontends using the strangler pattern, domain boundaries, shell integration, routing migration, shared design system, deployment safety, rollback, and observability.
Migrating a frontend monolith to micro frontends is not a simple refactor.
It is an architecture migration.
A bad migration creates:
Two broken systems
Duplicate UI
Routing conflicts
Shared state chaos
Deployment risk
Team confusion
Runtime failuresA good migration is incremental, domain-driven, measurable, and reversible.
The goal is not to rewrite everything.
The goal is to safely extract parts of the monolith into independently owned frontend applications while keeping the product working for users.
This article explains how to migrate a frontend monolith to micro frontends using a practical, interview-ready approach.
1. The Starting Point: Frontend Monolith
Most products start with one frontend app.
Example:
frontend-monolith
├── app
├── routes
├── pages
│ ├── home
│ ├── catalog
│ ├── product-details
│ ├── cart
│ ├── checkout
│ ├── orders
│ └── profile
├── shared
│ ├── ui
│ ├── hooks
│ ├── utils
│ └── api
└── stateThis is normal.
A monolith is not automatically bad.
It becomes a problem when it starts blocking teams and releases.
2. Why Migrate?
Migration should have a clear business or engineering reason.
Good reasons:
Multiple teams are blocked by one release cycle.
Build and test pipelines are too slow.
Ownership boundaries are unclear.
A single bug can delay full frontend release.
Teams need independent deployment.
The product has stable business domains.
The company wants incremental framework migration.Weak reasons:
Micro frontends are trendy.
A conference talk recommended it.
A big company uses it.
We want to use Module Federation.
We think splitting code automatically improves performance.Strong interview phrase:
I would migrate to micro frontends only when team ownership and release independence justify the complexity.
3. Migration Goal
The migration goal should be specific.
Bad goal:
Move frontend to micro frontends.Good goal:
Extract the catalog domain into an independently built and deployed remote while keeping the existing monolith stable.Better:
Reduce release coordination between Catalog and Checkout teams by extracting Catalog first, then Cart, then Profile, with rollback support at each step.A migration should improve:
Team ownership
Release independence
Build isolation
Deployment confidence
Runtime reliability
Observability4. Do Not Big-Bang Rewrite
The biggest mistake is trying to rewrite the full frontend at once.
Bad migration:
Stop feature development.
Create new shell.
Rewrite catalog.
Rewrite cart.
Rewrite checkout.
Rewrite profile.
Replace monolith after 12 months.Problems:
Huge delivery risk
No incremental value
Feature freeze pressure
Business changes during rewrite
Hidden bugs discovered late
Hard rollbackBetter migration:
Keep monolith running.
Extract one domain.
Compose new remote beside monolith.
Route small traffic.
Measure.
Stabilize.
Repeat.This is the strangler approach.
5. The Strangler Pattern
The strangler pattern means gradually replacing parts of an old system with a new architecture.
For frontend migration:
Existing Monolith
│
│ extract one domain
▼
Shell + Monolith + One Remote
│
│ extract next domain
▼
Shell + Multiple Remotes + Smaller Monolith
│
│ repeat
▼
Micro Frontend PlatformYou do not remove the monolith immediately.
You slowly shrink it.
6. Migration Stages
A practical migration can follow these stages:
Stage 0: Clean up monolith boundaries
Stage 1: Introduce shell/container
Stage 2: Extract first low-risk remote
Stage 3: Route traffic to remote
Stage 4: Add independent deployment
Stage 5: Add monitoring and rollback
Stage 6: Extract next domain
Stage 7: Retire monolith piecesThis avoids unnecessary risk.
7. Stage 0: Prepare the Monolith
Before extracting anything, improve the monolith structure.
If the monolith is a mess, extracting a remote will be painful.
Preparation steps:
Organize code by domain.
Reduce circular dependencies.
Move shared UI into design system.
Separate domain APIs.
Add route-level code splitting.
Add tests around critical flows.
Define ownership boundaries.Example target structure:
src/
├── features/
│ ├── catalog/
│ ├── cart/
│ ├── checkout/
│ ├── profile/
│ └── orders/
├── shared/
│ ├── ui/
│ ├── utils/
│ └── config/
└── platform/
├── auth/
├── routing/
└── analytics/This may already solve many problems.
Sometimes after modularizing the monolith, micro frontends may not be needed yet.
8. Identify Domain Boundaries
Good boundaries are business-domain boundaries.
Good candidates:
Catalog
Search
Product Details
Cart
Checkout
Orders
Profile
MarketingBad candidates:
Button
Product image
Price label
Header text
One small filter componentBoundary checklist:
Can one team own this area?
Can it be tested independently?
Can it be deployed independently?
Does it have clear route ownership?
Does it have limited communication with other areas?
Does it have its own API/domain model?
Can it fail without breaking the whole product?If the answer is no, the boundary may be wrong.
9. Choosing the First Remote
Do not start with the riskiest domain.
Bad first extraction:
Checkout
Payment
Authentication
Order placementThese areas are business-critical and failure-sensitive.
Better first candidates:
Marketing landing page
Product listing page
Profile page
Order history page
Recommendations widgetA good first remote should be:
Visible enough to prove value
Low enough risk to recover safely
Clear in ownership
Limited in cross-app communication
Not deeply tied to checkout/paymentExample first extraction:
Catalog RemoteWhy?
Clear domain
Owns category/listing pages
Can be route-owned
High value
Usually safer than checkout10. Introduce the Shell App
At some point, you need a shell or container.
The shell owns:
Global layout
Top-level routing
Navigation
Authentication bootstrap
Remote loading
Error boundaries
Fallback UI
Feature flag bootstrap
Analytics initializationDuring migration, the shell may compose both:
Old monolith routes
New remote routesExample:
Shell App
├── / → Monolith Home
├── /categories/:slug → Catalog Remote
├── /cart → Monolith Cart
├── /checkout → Monolith Checkout
└── /profile → Monolith ProfileThe shell becomes the migration bridge.
11. Coexistence Architecture
┌──────────────────────┐
│ Shell App │
│ Routing/Auth/Layout │
└──────────┬───────────┘
│
┌────────────────────┴────────────────────┐
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ Existing Monolith │ │ Catalog Remote │
│ Cart/Checkout/Profile │ │ Product Listing │
└──────────────────────┘ └──────────────────────┘This lets the team extract gradually.
The user should not feel the difference.
12. Routing Migration
Routing is one of the hardest parts.
Before migration:
Monolith owns all routes.During migration:
Shell owns top-level route decisions.
Monolith owns old routes.
Remote owns extracted route subtree.Example:
/categories/:slug → Catalog Remote
/cart → Monolith Cart
/checkout → Monolith Checkout
/profile → Monolith ProfileAfter more extraction:
/categories/:slug → Catalog Remote
/cart → Cart Remote
/checkout → Checkout Remote
/profile → Profile RemoteImportant:
Deep links and refresh must work throughout migration.
13. Handling Deep Links
Deep links must keep working.
Example:
/categories/shoes?page=2&sort=priceExpected flow:
User opens URL directly
│
▼
Shell loads
│
▼
Shell matches /categories/:slug
│
▼
Catalog Remote loads
│
▼
Catalog Remote reads query params
│
▼
Products renderDo not rely on in-memory navigation state.
Use URL state for route-level state like:
Search query
Filters
Sorting
Pagination
Category slug
Selected tab14. Shared Design System Migration
Before extracting remotes, stabilize the design system.
If every part of the monolith uses different UI patterns, micro frontends will make inconsistency worse.
Design system migration steps:
Define design tokens.
Create shared components.
Migrate common UI patterns.
Add visual regression tests.
Document usage guidelines.
Version the design system.
Make remotes consume it consistently.Bad:
Catalog remote uses its own buttons.
Cart remote uses another modal system.
Checkout remote invents new form components.Good:
All remotes use shared design tokens and approved components.15. Shared State Migration
Shared state must be handled carefully.
Monoliths often have one global store.
Example:
Redux Store
├── auth
├── catalog
├── cart
├── checkout
├── profile
└── ordersDuring migration, avoid exposing the whole store to every remote.
Better approach:
Shell owns platform state.
Remotes own domain state.
Backend owns business-critical state.| State | Recommended Owner |
|---|---|
| Auth summary | Shell/auth provider |
| Theme | Shell/platform |
| Locale | Shell/platform |
| Product filters | Catalog Remote + URL |
| Cart | Cart API + Cart Remote |
| Checkout | Checkout API/session + Checkout Remote |
| Orders | Orders API + Orders Remote |
| Profile | Profile API + Profile Remote |
Avoid migrating by simply sharing the old global store across all remotes.
That creates a distributed monolith.
16. API and Backend Migration
Frontend boundaries should align with backend/API ownership where possible.
Example:
Catalog Remote → Catalog API
Cart Remote → Cart API
Checkout Remote → Checkout API
Profile Remote → Profile API
Orders Remote → Orders APIIf the monolith currently uses one large API client, gradually split it.
Bad:
Every remote imports one giant shared API client with all domains.Good:
Each remote owns or consumes its domain API client.For complex aggregation, consider BFF patterns.
17. Communication During Migration
During coexistence, the monolith and remote may need to communicate.
Use explicit contracts.
Recommended:
URL state
Backend state
Small custom events
Shell-mediated platform contextAvoid:
Remote directly mutates monolith store.
Monolith imports remote internals.
Remote reads hidden localStorage keys from monolith.Example:
Catalog Remote emits product:viewed
Shell analytics records event
Monolith does not need to know remote internals18. Deployment Migration
Initially, the extracted remote may deploy with the monolith.
But the final goal is independent deployment.
Migration steps:
Step 1: Remote code extracted but deployed with monolith.
Step 2: Remote has separate build.
Step 3: Remote artifact published separately.
Step 4: Shell loads remote at runtime.
Step 5: Remote has independent CI/CD.
Step 6: Remote can be rolled back independently.Do not claim success until independent deployment and rollback are working.
19. Remote Manifest During Migration
Use a manifest to control which remote version the shell loads.
Example:
{
"catalogApp": {
"version": "1.0.0",
"url": "https://cdn.company.com/catalog/1.0.0/remoteEntry.js",
"owner": "catalog-team",
"rollbackVersion": "0.9.8"
}
}Benefits:
Controlled rollout
Rollback support
Environment promotion
Per-remote version visibility
Release governanceDuring migration, this gives safety.
20. Rollback Strategy
Every extraction must have a rollback plan.
Example:
Catalog Remote v1.0.0 fails in production.Rollback options:
Route /categories back to monolith.
Update manifest to previous remote version.
Disable feature flag for remote route.
Show fallback UI.The safest migration includes a route-level fallback.
Example:
/categories/:slug
├── primary: Catalog Remote
└── fallback: Monolith Catalog PageRollback must be tested before release.
21. Feature Flags for Migration
Feature flags help control migration risk.
Use flags for:
Enable new catalog remote
Route percentage of users to remote
Enable new cart drawer
Enable new profile page
Disable broken remote quicklyExample:
catalog_remote_enabled = true
catalog_remote_rollout_percentage = 10Rollout plan:
Internal users → 1% → 10% → 25% → 50% → 100%Monitor at each stage.
22. Testing Migration
Migration requires extra testing.
Test:
Old route still works.
New remote route works.
Deep links work.
Refresh works.
Auth works.
Shared layout works.
Analytics works.
Fallback to monolith works.
Remote loading failure is handled.
E2E critical journeys still pass.Example E2E journey:
Open category page
Apply filter
Open product
Add to cart
View cart
CheckoutIf catalog is migrated but checkout is still monolith, the cross-boundary journey must still work.
23. Observability During Migration
You need to know whether the new remote is healthier than the old monolith route.
Track:
Remote load success/failure
Remote runtime errors
Remote version
Route
Shell version
Fallback usage
Conversion impact
Web Vitals
API errors
User journey failuresExample:
{
"route": "/categories/shoes",
"renderer": "catalogRemote",
"remoteVersion": "1.0.0",
"shellVersion": "2.4.1",
"event": "remote_loaded"
}Compare:
Monolith catalog route performance
vs
Catalog remote route performanceMigration should be measured, not assumed successful.
24. Team Ownership During Migration
Migration fails without ownership.
Define:
| Area | Owner |
|---|---|
| Shell app | Platform Team |
| Catalog extraction | Catalog Team |
| Design system | Design System Team |
| Routing migration | Platform + Domain Team |
| CI/CD setup | Platform Team |
| Monitoring | Platform + Domain Team |
| Rollback process | Platform + Domain Team |
Each remote should have:
Code owner
Deployment owner
Monitoring owner
Incident owner
Documentation ownerNo ownership means no accountability.
25. Migration Roadmap Example
A practical migration roadmap:
Phase 1: Modularize monolith
Phase 2: Introduce shell routing layer
Phase 3: Extract Catalog Remote
Phase 4: Add manifest-based loading
Phase 5: Add monitoring and rollback
Phase 6: Extract Profile Remote
Phase 7: Extract Cart Remote
Phase 8: Extract Orders Remote
Phase 9: Extract Checkout Remote last
Phase 10: Retire old monolith routesCheckout is extracted later because it is business-critical.
26. What to Extract First
Good first candidates:
Marketing landing page
Catalog listing
Profile page
Order history
Recommendations widgetPoor first candidates:
Checkout payment
Authentication
Order placement
Complex shared cart logicDecision table:
| Candidate | First Extraction? | Reason |
|---|---|---|
| Marketing page | Good | Low risk |
| Catalog page | Good | Clear domain |
| Profile page | Good | Clear ownership |
| Cart | Medium | More shared state |
| Checkout | Poor initially | Critical flow |
| Auth | Poor initially | Platform-critical |
27. Common Migration Anti-Patterns
| Anti-Pattern | Why It Is Bad |
|---|---|
| Big-bang rewrite | High risk and slow value |
| Extracting checkout first | Too risky |
| No rollback path | Production incidents become severe |
| Sharing old global store everywhere | Creates distributed monolith |
| No shell ownership model | Routing becomes chaotic |
| No design system | UI inconsistency grows |
| No contract tests | Runtime breakage |
| No observability | Migration health unknown |
| Too many tiny remotes | Operational overhead |
| Migrating without business reason | Architecture vanity |
28. Interview Questions
Q1. How would you migrate a frontend monolith to micro frontends?
I would use an incremental strangler approach. First, I would modularize the monolith and identify domain boundaries. Then I would introduce a shell that can route to both old monolith pages and new remotes. I would extract one low-risk, high-value domain first, such as catalog or profile, deploy it independently, monitor it, and add rollback. After it stabilizes, I would repeat with other domains.
Q2. What would you extract first?
I would avoid extracting checkout or authentication first because they are critical flows. I would start with a domain that has clear ownership and limited coupling, such as catalog listing, profile, order history, or a marketing page.
Q3. How do you reduce migration risk?
Use feature flags, route-level fallback, contract tests, remote loading fallback UI, manifest-based versioning, canary rollout, monitoring, and rollback to monolith or previous remote version.
Q4. How do you handle shared state during migration?
I would avoid exposing the old global store to all remotes. Platform-level state like auth, theme, and locale can be provided by the shell. Domain state should live inside the owning remote or backend APIs. Business-critical state like cart and checkout should be backend-first.
Q5. How do you know the migration is successful?
The migration is successful when the extracted domain is independently owned, independently deployed, monitored, rollback-safe, integrated with routing/auth/design system, and no longer blocks or depends on the monolith release cycle.
29. Strong Senior Answer
If an interviewer asks:
“How would you migrate an existing frontend monolith to micro frontends?”
A strong answer:
I would not do a big-bang rewrite. I would use a strangler-style migration. First, I would clean up the monolith by organizing it around domains like catalog, cart, checkout, profile, and orders. Then I would introduce a shell app that owns top-level routing, auth bootstrap, layout, remote loading, and fallback UI. Next, I would pick one low-risk, high-value boundary such as catalog or profile. I would extract it as a remote, compose it beside the monolith, and route only that path to the new remote. I would use feature flags and a remote manifest so we can roll out gradually and roll back quickly. I would avoid extracting checkout or auth first because those are critical flows. I would also avoid sharing the old global store across remotes because that creates a distributed monolith. Instead, I would move toward URL state, backend-owned business state, and explicit event contracts. The migration is only successful when the extracted remote has independent CI/CD, contract tests, observability, clear ownership, and rollback support.
30. Final Migration Checklist
- Is the domain boundary clear?
- Does one team own it?
- Is it low enough risk for first extraction?
- Are route boundaries defined?
- Do deep links work?
- Is shared UI available through design system?
- Is state ownership clear?
- Are communication contracts documented?
- Can the shell load the remote safely?
- Is fallback UI available?
- Is route-level rollback possible?
- Are feature flags available?
- Are contract tests in place?
- Is monitoring per remote available?
- Can the remote deploy independently?
- Can it roll back independently?
31. Summary
Migrating from a frontend monolith to micro frontends should be incremental.
Do not rewrite everything.
Do not start with the riskiest flow.
Do not share the old global store everywhere.
A good migration follows this path:
Modularize monolith
Identify domains
Introduce shell
Extract one remote
Route gradually
Monitor
Rollback if needed
Repeat
Retire old monolith piecesThe strongest takeaway:
A successful micro frontend migration is not measured by how much code was split. It is measured by whether teams can own, deploy, monitor, and roll back their domains independently.
References
- Micro Frontends — Martin Fowler (https://martinfowler.com/articles/micro-frontends.html)
- Micro Frontends (https://micro-frontends.org)
- AWS Prescriptive Guidance: Micro-frontends (https://docs.aws.amazon.com/prescriptive-guidance/latest/micro-frontends-aws/introduction.html)
- webpack Module Federation Documentation (https://webpack.js.org/concepts/module-federation/)
- Module Federation Official Site (https://module-federation.io)