Skip to content

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 benefitNeeds micro-frontends?
Independent deployment per teamYes
Independent release cadenceYes
Enforced code boundariesNo — a lint rule does this
Smaller bundlesNo — usually worse, from duplication
Different frameworks per teamYes, and it is rarely a good idea
Incremental migration off a legacy appOften the best reason of all
Only the first row requires micro-frontends. The rest are available from a well-structured single application.

The costs, in the order you will encounter them

  1. 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.

  2. 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.

  3. 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.

  4. 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:

  1. 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.

  2. Genuinely different release cadences, mandated

    A checkout surface under change control alongside a marketing surface that ships hourly.

  3. 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:

  1. Feature folders with an enforced dependency direction

    features/checkout may import components/; components/ may never import features/. One lint rule, and it is the same guarantee a network boundary provides.

  2. 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.

  3. Route-level code splitting

    You get the bundle benefit people attribute to micro-frontends, with no duplication, because the bundler sees the whole graph.

  4. Ownership files rather than repositories

    CODEOWNERS gives 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 independenceHigh — separate repos, separate reviewsHighest
Deploy independenceNo — consumer must rebuildYes
Duplicate dependenciesNone — one bundle graphReal, and needs managing
Version skew at runtimeImpossiblePossible, and a source of bugs
DebuggingNormalNeeds cross-boundary tooling
Build-time composition keeps most of the independence and avoids the runtime costs entirely.

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

  1. 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.

  2. 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.

  3. Contracts between remotes are versioned APIs, not shared objects

    Events and props, never a mutable global.

  4. Every remote must render standalone

    If it cannot, it is not independent, and you have a distributed frontend.

  5. 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.