Writing
Micro-frontends or a modular monolith: which one your product actually needs
5 min read
Micro-frontends are the front-end version of an argument the back end had ten years ago, and they arrive at the same answer: the technique solves a team coordination problem, and if you do not have that problem you are paying a runtime cost for nothing.
What they actually buy
One thing, and it is genuinely valuable when you need it: several teams deploying to one product surface without coordinating releases.
Everything else usually attributed to them is available more cheaply.
| Claimed benefit | Needs micro-frontends? |
|---|---|
| Independent deployment per team | Yes |
| Independent release cadence | Yes |
| Enforced code boundaries | No — a lint rule does this |
| Smaller bundles | No — usually worse, from duplication |
| Different frameworks per team | Yes, and it is rarely a good idea |
| Incremental migration off a legacy app | Often the best reason of all |
The costs, in the order you will encounter them
Duplicate dependencies
Two remotes both using React ship React twice unless you share it — and sharing it means they must agree on the version, which is precisely the coupling the architecture was meant to remove. This is the trade at the heart of it and there is no clean escape.
Design system drift
Shared components are versioned, so remotes upgrade at different times. Two buttons, two spacings, one product. This shows up within months and is very visible to users.
Cross-application state and routing
Auth, feature flags, navigation and toasts all span remotes. Every one of them needs a contract, and every contract is a coupling with a version.
Debugging and performance across boundaries
A slow page is now several deployables. Attribution needs infrastructure that a single application gets for free from a profiler.
The three conditions that justify them
Not "we are getting big". Specific, observable facts:
Multiple teams are blocked on one another's deploys
One pipeline, and a revert for one team is a revert for all of them. This is the real signal, and it is organisational rather than technical.
Genuinely different release cadences, mandated
A checkout surface under change control alongside a marketing surface that ships hourly.
Incremental migration off something you cannot rewrite
Strangling a legacy application route by route is the strongest technical case there is — and it is temporary by design, which is what makes it a good trade.
If none of these is true, the modular alternative gives you the boundaries at none of the cost.
Getting the independence without the runtime cost
Most teams asking for micro-frontends want enforced boundaries and independent work — both available inside one application:
Feature folders with an enforced dependency direction
features/checkoutmay importcomponents/;components/may never importfeatures/. One lint rule, and it is the same guarantee a network boundary provides.One public entry point per feature
Forbid deep imports across feature folders. Now a feature's internals are genuinely internal, and refactoring inside one cannot break another.
Route-level code splitting
You get the bundle benefit people attribute to micro-frontends, with no duplication, because the bundler sees the whole graph.
Ownership files rather than repositories
CODEOWNERSgives per-directory review requirements without splitting the deployable.
If you do split: build-time before runtime
There is a middle option that is under-used, and it is usually the right first step.
| Build-time (packages) | Runtime (module federation / zones) | |
|---|---|---|
| Team independence | High — separate repos, separate reviews | Highest |
| Deploy independence | No — consumer must rebuild | Yes |
| Duplicate dependencies | None — one bundle graph | Real, and needs managing |
| Version skew at runtime | Impossible | Possible, and a source of bugs |
| Debugging | Normal | Needs cross-boundary tooling |
If build-time composition is insufficient specifically because teams cannot wait for each other's rebuild, then runtime composition is the right answer — and now you are buying it for the reason it exists.
Rules if you go runtime
One shared design system, versioned, with a supported-range policy
And a maximum lag — "no remote may be more than one minor behind" — enforced in CI, or drift is guaranteed.
Share the framework as a singleton and pin the range narrowly
Two copies of React in one page is not a performance problem, it is a correctness one.
Contracts between remotes are versioned APIs, not shared objects
Events and props, never a mutable global.
Every remote must render standalone
If it cannot, it is not independent, and you have a distributed frontend.
Budget the shell's performance separately
The shell loads on every route and is the easiest place for a regression to hide.
The honest default
Start with one application, feature folders, an enforced dependency direction and route-level splitting. That covers the large majority of products, including quite large ones.
Move to micro-frontends when a team fact forces it — not when the architecture diagram would look more modern, and not to get boundaries that a lint rule already gives you for free.
Start here
Tell us what you are building
Or what is breaking, or what has to go faster. You will get a straight answer from an engineer who would do the work.