The customer success platform we have built for a US software company since 2018 — seven engineers at launch — is, at its core, a set of dashboards. Each one is a grid of widgets — account health, usage trends, financial signals, support activity — and which widgets appear depends on which of the customer's own systems are connected. Connect a support tool and the ticket widgets light up. Do not, and they are simply absent.
The platform was built in AngularJS. As it matured it became clear that React's component model fitted what the UI actually was considerably better: independent units, each responsible for fetching, caching, refreshing and invalidating its own data, composed into a page.
This is the moment every long-lived product reaches. The framework choice that was correct at the start is no longer correct, and the product is now too large to casually change it.
The two answers that do not work
Rewrite it
Months of engineering producing no new customer value, behind a feature freeze the business will not agree to — and if it does agree, will quietly violate. Rewrites of live products have a poor completion record for exactly this reason.
Rejected: the freeze is not available.
Run both, forever
New work in React, old screens left in Angular, two frameworks in one application indefinitely. Cheap on day one and a permanent tax thereafter, with no plan for it ever to end.
Rejected: no exit.
Most teams pick B while telling themselves it is a temporary version of A.
The third answer
We chose micro front-ends. Each widget became an independently deployable front-end unit, connected to its own service on the back end. The Angular dashboards kept working while new widgets arrived in React, and the migration proceeded widget by widget, on the product team's schedule — as a property of the architecture rather than as a project with a deadline.
The structural argument is the one worth carrying away:
Widget-based micro front-ends give the front end what microservices give the back end — independent deployability, contained failure, and the ability for different parts of one screen to evolve at different speeds.
And in this case the architecture ends up mirroring the domain. The product's premise is configurable, independently-toggled widgets. Making them independently-deployable units means the technical boundaries and the product boundaries are the same boundaries — which is usually the sign that a decomposition is right rather than merely convenient.
The alternative shape, for a product at this size, was a monolithic single-page application talking to a monolithic back end. In that world every widget change risks every other widget, and the blast radius of a bad deploy is the entire dashboard.
What it costs
This is where most write-ups of the pattern stop, so it is worth being specific. Micro front-ends are not free and the costs are real:
- Two runtimes, for a while. During the transition the browser loaded both frameworks. That is a bundle-size and parse-time cost paid by every user on every page load, and it is the reason the transition should have an end date even if individual widgets do not.
- Shared state needs a contract. Widgets that were siblings in one application are now independent units that still need to agree on the selected account, the date range, the current user. That coordination has to be designed explicitly rather than inherited from a shared store.
- Styling drifts. Independently developed units diverge visually unless a design system is genuinely enforced rather than merely documented. Users do not care about the architecture and will notice two shades of the same button.
- Build and deploy complexity rises. More units, more pipelines, more versioning, more ways for a composition to be broken by something that built fine on its own.
The honest framing: micro front-ends trade a large one-time cost for a smaller recurring one — recurring only while the transition lasts, which is the detail that decides whether the trade was worth it.
It finished, and that is the actual claim
The migration completed. The platform is entirely React, the old framework is gone, and the two-runtime cost went with it.
This matters more than it sounds, because incremental migrations have a notorious failure mode: they stall. The easy eighty per cent moves, the remaining screens are the awkward ones nobody wants to touch, and the temporary dual-framework state quietly becomes the permanent architecture — which is option B from earlier, arrived at by a more expensive route.
What prevented that here was not discipline or a tracked burndown. It was that replacing a unit became cheaper than not replacing it. Once the seams existed, any widget already being modified was easier to rewrite in the new framework than to keep patching in the old one. The migration proceeded as a side effect of ordinary product work — a widget here because a customer needed a change, another there because a new integration touched it — until there was nothing left.
At no point was there a feature freeze, a migration team, or a quarter where the roadmap paused.
That is the difference between a migration strategy and a migration project. A project has a plan, a deadline and a sponsor, and it competes with product work for all three. A strategy changes the relative cost of two options so that the preferred one happens on its own.
The stall test
The question that decides it is not can we migrate incrementally — almost anything can. It is: will each individual replacement be locally worth doing?
If replacing a unit is cheaper than continuing to maintain it, the migration completes without anyone managing it. If it is more expensive, and only justified by the eventual end state, the migration is relying on organisational will to sustain something that is locally irrational — and organisational will is exactly what runs out in month nine, when a customer escalation arrives and the migration is the obvious thing to deprioritise.
When not to
The pattern needs seams. This product had them: widgets were already independent, already toggled, already backed by separate services.
A tightly coupled interface — a complex form, a document editor, an interactive canvas — has no such seams, and adopting micro front-ends there means inventing boundaries that do not exist in the domain to gain a property nobody needs. It spends the coordination cost without the decomposition benefit, and ends up with a distributed monolith in the browser, which is worse than either honest option above.
The precondition is not framework choice. It is whether the UI is genuinely a composition of independent things — and that is a question about the product, not about the front end.
The generalisable part
When the technology under a live product becomes wrong, the framing that traps teams is a binary: replace it or live with it. Both are decisions about the whole system at once, which is why both are unaffordable.
The escape is to find a boundary at which the old and the new can coexist without knowing about each other, and make the migration a property of ordinary work rather than a project. Ask what the smallest independently replaceable unit is — a widget, a route, a service, a table — and whether the system can tolerate a mixture at that granularity. If it can, you can migrate incrementally and stop worrying about finishing, because partial completion is a stable state rather than a failure.
If it cannot tolerate a mixture at any granularity, that is worth knowing too — it means the coupling is the actual problem, and the framework was never the thing to fix.