What you'll learn: By the end of this lesson you will be able to recognise the common ways a micro-frontend system loses its independence (lockstep releases, shared-everything, chatty fragments, fragments cut too small, visible seams, a migration that never finishes), test a proposed split against the reason for splitting, and plan a migration from a front-end monolith with a routing facade, a first slice, flag-based traffic ramping and a way back.
Imagine renovating a shop that has to stay open. You do not close it for a year and reopen with a new interior. You rebuild one aisle at a time, put up a temporary wall, and move customers through the finished part while the rest is still in use. Martin Fowler's name for the software version is the strangler fig: a new system grows around the old one and gradually replaces it. He says a simple replacement plan is one he has seen "go down in flames most of the time", and notes that small replacement pieces carry less risk and let the business reap value earlier.
The running example is an online shop built as one large React application with one build and one release train, owned by many teams. All details are illustrative. This lesson is the cautionary and practical end of the micro-frontend module: for the style itself see the lessons before it in the course (monolith versus micro-frontends, composition approaches and shared dependencies are planned lessons, so they are not linked yet). For the back-end version of the migration, Strangler Fig Migration has the deep treatment; here we add what is specific to a browser.
The core idea
- Micro-frontends exist to give teams independent delivery. Cam Jackson's article on martinfowler.com (June 2019) defines them as "independently deliverable frontend applications" composed into a greater whole. If the pieces still have to ship together, you pay the costs of distribution and keep the coupling.
- Most anti-patterns are coupling that crept back in through shared code, shared state, shared release steps or shared design decisions.
- A migration is a sequence of reversible routing decisions, not a rewrite. Put a facade in front, move one slice, ramp traffic, and be able to go back.
Anti-patterns
The term distributed monolith is in common use but is not defined by a standard, and the primary sources opened for this lesson do not define it. In this lesson it means a system split into separately built pieces that still have to change and release together. The detection signals below are judgement, not measurements from a study.
| Anti-pattern | What it looks like | Why it hurts | A check | A fix |
|---|---|---|---|---|
| Distributed monolith | Fragments are npm packages built into one bundle by the shell, so any change needs a shell release | The article calls this build-time "lockstep release process" painful enough to "recommend strongly against" | Can one team ship to production without anyone else? | Runtime composition, or a route-level split with its own deploy |
| Shared-everything | One global store, one shared component library that holds business rules, exact shared dependency versions | Jackson: externalised libraries create "an implicit contract" that everyone uses the same versions; shared components should hold only UI logic | Does a library change need three teams to approve? | Share design tokens and UI primitives only; keep domain logic in the owning fragment |
| Chatty fragments | Fragments call each other's functions or watch each other's state | Every call is a hidden dependency | How many cross-fragment events exist, and who owns each? | The article advises communicating "as little as possible"; use the URL and a few named events as the contract |
| Nano-fragments | A fragment per button or card | Vercel's guide: component-level migration brings "extra orchestration, version-skew, and state-sharing challenges" | Can a fragment be shipped and tested alone in a way the user would notice? | Merge fragments up to a page or a flow |
| Visible seams | Two headers, two spinners, a different focus style per area | Users see the team structure | Walk a task end to end on a phone | Choose slices by user journey; share navigation and tokens |
| Eternal migration | Two stacks live for years | Both must be patched, hosted and understood | Is there a date and an owner for the last legacy route? | Set milestones and delete as you go (judgement) |
| No shell owner | The shell is "everyone's" | Nobody owns auth, routing or error reporting | Who is paged for a shell bug? | Name a platform owner |
Figure 1. Is this split worth it?
shared releases?"} -->|"no"| M["Keep the monolith
and make it modular"] Q1 -->|"yes"| Q2{"Can the split follow
user journeys?"} Q2 -->|"no"| M Q2 -->|"yes"| Q3{"Can each piece ship
without the others?"} Q3 -->|"no"| X["You are building a
distributed monolith"] Q3 -->|"yes"| S["Split, and share only
tokens and UI primitives"]
How to read this: the middle question matters most. A split that cannot ship alone adds network and tooling cost and removes none of the coupling.
Shared state and dependencies, the quiet coupling
Two cases deserve their own paragraph because they are easy to miss.
Shared state. Jackson's article says micro-frontends should avoid shared state and communicate by sending messages or events. A global variable on window, a shared Redux store, or a shared localStorage key is shared state, whatever it is called. When the stacks differ, as they do mid-migration, the problem gets worse. Decide who owns each piece of data. For data that matters beyond one screen, such as the cart, the owner is a backend API that both sides read, not a front-end variable.
Shared dependencies. The same article notes that independently built bundles can duplicate common dependencies, and that sharing them externally brings "an implicit contract" about exact versions. Treat that as a trade-off to decide once and write down, not as a default. Module Federation and independent deployment have their own lessons later in this course.
Migrating with a strangler facade
The facade is the one component that decides, per request, which application serves it. In the browser world the seam is usually the URL. Vercel's migration guide recommends starting page by page ("easier and recommended first"), warns that slices must be chosen well to avoid user-experience seams, and describes routing by feature flag: users are sent to the old or new application depending on a flag, so traffic can be switched gradually after the new one is verified. Component-by-component migration is described as more challenging, with extra orchestration, version skew and state sharing, and the guide notes that performance can suffer when both stacks run in one application.
Illustration 1, minimal. A facade as a function of path and user. The percentages are illustrative.
const ROUTES = [
{ prefix: "/account", app: "new", rolloutPercent: 100 },
{ prefix: "/search", app: "new", rolloutPercent: 25 },
];
function bucket(userId) { // stable number 0..99 per user
let h = 0;
for (const ch of userId) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
return h % 100;
}
export function chooseApp(path, userId) {
const route = ROUTES.find((r) => path === r.prefix || path.startsWith(r.prefix + "/"));
if (!route) return "legacy";
return bucket(userId) < route.rolloutPercent ? route.app : "legacy";
}
A Node 26 run printed new for chooseApp("/account/orders", "alice") and legacy for chooseApp("/cart", "alice") (no route matches). For /search, bob (bucket 17) got new and alice (bucket 40) got legacy. Over 10,000 generated user ids, 25.13 percent got the new page. The same user always gets the same answer, so nobody flips between versions on every request. Setting rolloutPercent to 0 sends everyone back to the legacy app, which is the way back.
Figure 2. A facade in front of two applications
How to read this: only the facade knows how traffic is split. Both stacks read the same backend, so state that matters lives in one place. The 25 percent is illustrative.
The steps, in order
- Put the facade in place with no change in behaviour. Everything routes to legacy. This proves the facade and its rollback before any risk.
- Pick the first slice. Vercel's guide advises low-risk, high-value pages first, and grouping related pages in the same batch to preserve a coherent experience. A page that changes often is a good candidate, because the new setup pays back sooner (the guide also says so).
- Agree the thin shared layer. Navigation, design tokens, error reporting and a session that both stacks accept. See Sharing Authentication Across Micro-Frontends for the session, and Failure Isolation and Observability for reports that carry the same fragment and version fields in both stacks.
- Ramp with a flag. Start at a small share, verify functionally and with analytics, then raise it. Strangler Fig Migration covers ramping rules in depth.
- Decommission. When a route is at 100 percent and quiet, delete the legacy route and its code. This step is the one that gets skipped; set a date and an owner when you start (judgement, not a quote).
Figure 3. One route moving from legacy to new
How to read this: reversibility lasts until the legacy code is deleted. That is why deletion is a decision with an owner, not an afterthought.
Figure 4. What the facade does on each request
How to read this: the choice is made once per request from stable inputs, so a user's experience does not flicker.
| Strategy | Good for | Costs | Risk |
|---|---|---|---|
| Page by page | Pages with clear boundaries | Seams if slices cut a journey | Low (the guide recommends it first) |
| Component by component | No page boundary exists | Orchestration, version skew, shared state | Higher |
| Rewrite, then switch | Rarely anything | Long wait, details of old behaviour are missed | Fowler has seen such plans "go down in flames most of the time" |
In practice: moving search and account out of a front-end monolith
This is a realistic scenario, not a case history. It is a composite built from common production patterns. It describes no specific company or product, and every name and number in it is an illustrative assumption.
The situation
The shop's single React application has one release train. A change to search waits for the checkout team's freeze. Teams want to ship search and account pages on their own schedules. Leadership wants no big-bang cutover.
Starting architecture
Figure 5. Before: one application, one release
How to read this: every change goes through the same build and the same freeze.
Options considered
| Option | What it fixes | What it costs |
|---|---|---|
| Keep one app, make it modular | Cleaner code | Release train unchanged |
| Rewrite everything | Clean slate | Long wait, high risk |
| Strangle: facade plus search and account slices | Independent deploys for those routes | A facade, a thin shared layer, two stacks for a while |
The decision
Judgement, labelled as such: strangle in this order: facade with no change, account (low risk), then search (high change rate), with the cart and checkout last. Share only navigation, tokens, a session and an error-reporting contract. Set a date to delete each legacy route when it starts.
Rollout and verification
- Ship the facade with every route on legacy and confirm no behaviour change.
- Ramp account to 100 percent, watching error rates and task completion.
- Ramp search in steps, with the flag as the way back.
- Delete legacy code for each route after a quiet period you agree up front.
The 2 a.m. failure
Customers report an empty cart after adding items on the new search page. The new slice kept its own cart count in a store; the legacy header still read the count from a global variable the old code set. Items were added on the server, but the header and the legacy cart page showed stale values for users in the ramp.
First three checks: (1) compare affected users' bucket and route; (2) confirm the server cart is correct; (3) look for any data that two stacks both write. Mitigation: set search's rolloutPercent to 0. Follow-up: make the backend cart API the only source, have both stacks read it, and add a test that adds an item through each stack.
Architect's notes
- Start from the reason. Write down which team is blocked by which release step. If you cannot, do not split.
- Make the first slice boring. Prove the facade and rollback on a low-risk route.
- Put one owner on the shell and the facade.
- Keep the shared layer thin and versioned. Everything you share is a contract.
- Schedule the deletion when you schedule the migration.
- Measure release frequency per team, lead time for a change, and the share of traffic on each stack.
Common mistakes
- Splitting by technical layer (header, footer, list) instead of by user journey.
- Sharing a store "temporarily". Temporary shared state tends to stay.
- Leaving the migration half-done because the last routes are hard.
- Migrating component by component first when a page-level split was possible.
- No way back, because the flag was removed before the legacy route was safe to delete.
- Judging success by the number of fragments, not by independent releases.
Key takeaways
- The goal is independent delivery; if pieces ship together you built a distributed monolith (a common informal term, not a standard).
- Build-time package integration forces a lockstep release, which the original article recommends strongly against.
- Share design tokens and UI primitives; keep domain logic and state with their owner.
- Communicate between fragments as little as possible, through the URL and a few named events.
- A strangler migration puts a routing facade in front and moves one slice at a time.
- Page-by-page migration is easier and recommended first; component-by-component is harder.
- Ramp with a stable per-user flag and keep a way back until you delete the old route.
- Set a deletion date and an owner when you start.
Practice exercises
Exercise 1, Starter (30 min): Take a monolith you know. List the last five features and write down which teams had to coordinate. Is that a reason to split?
Exercise 2, Intermediate (2 hours): Implement chooseApp with a test that proves the same user always gets the same app and that setting the percentage to 0 sends everyone to legacy.
Exercise 3, Advanced (half day): Plan a migration for a shop with five areas (home, search, product, cart, account). Choose the order, name the shared layer, and write the exit criterion and deletion date for each route.
Related reading
In this course:
- Sharing Authentication Across Micro-Frontends
- Failure Isolation and Observability in Micro-Frontends
Elsewhere on the site:
- Strangler Fig Migration, the facade, ramping, data and the finish line from the back end's point of view.
Official documentation
- Martin Fowler: Micro Frontends (Cam Jackson, 2019)
- Martin Fowler: Strangler Fig Application (2024)
- Vercel: Incremental migrations with microfrontends, a vendor guide; read it as one source, not a standard