Micro Frontend Architecture

How to break up a frontend monolith, what it buys you, and what it costs.

The Problem

At some scale, frontend codebases start to exhibit the same symptoms as backend monoliths:

This is the problem Micro Frontend Architecture (MFE) exists to solve.

What Is a Micro Frontend?

The idea is simple: apply the same decomposition philosophy from microservices to the frontend. Instead of one team shipping one big application, multiple teams independently build, test, and deploy their own slices of the UI.

Each micro frontend:

The user sees a single coherent app. Under the hood, it’s stitched together from independently-deployed pieces.

Architecture Overview

A typical MFE setup has three layers:

1. Shell Application (Host)

The shell is a thin container that owns the URL router and is responsible for loading and mounting micro frontends. It has minimal business logic. Its job is composition.

2. Micro Frontends (Remotes)

Each MFE exposes a specific surface area of the UI — header, dashboard, cart, profile. They are independently built and deployed, usually to a CDN.

3. Shared Foundation

A design system, auth utilities, and shared state primitives that every MFE consumes. This is deliberately kept small — the more you put here, the more coupling you reintroduce.

Micro Frontend Architecture diagram

Editable source: mfe-architecture.drawio — open it at app.diagrams.net (File → Open From → Device).

The Implementation: Module Federation

The dominant runtime approach today is Webpack 5 Module Federation. It lets one webpack build (the shell) dynamically load JavaScript modules from another webpack build (the remote) at runtime.

A remote exposes a component:

// cart/webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "cart",
      filename: "remoteEntry.js",
      exposes: {
        "./CartWidget": "./src/CartWidget",
      },
      shared: { react: { singleton: true }, "react-dom": { singleton: true } },
    }),
  ],
};

The shell consumes it:

// shell/webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "shell",
      remotes: {
        cart: "cart@https://cdn.example.com/cart/remoteEntry.js",
      },
    }),
  ],
};

And the shell loads the cart component lazily:

const CartWidget = React.lazy(() => import("cart/CartWidget"));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <CartWidget />
    </Suspense>
  );
}

The cart team can deploy a new version of their bundle to the CDN, and the shell picks it up on the next page load — no shell redeploy required.

Communication Between MFEs

MFEs should be as decoupled as possible, but they do need to talk. Three patterns in increasing order of coupling:

Custom Events — fire-and-forget, no shared state:

// cart MFE dispatches
window.dispatchEvent(new CustomEvent("cart:updated", { detail: { count: 3 } }));

// header MFE listens
window.addEventListener("cart:updated", (e) => setCartCount(e.detail.count));

Shared URL / Query Params — navigation-based state, naturally serializable.

Shared State Module — a tiny state package in the shared foundation (zustand store, Redux slice) that multiple MFEs import. Use sparingly; this is the most coupling-heavy option.

The Real Trade-offs

MFE solves real problems, but it introduces real complexity. Be honest with yourself about which side of this line you’re on:

You need MFE if…You don’t if…
5+ teams own different parts of the UI1-2 teams own the whole frontend
Teams need to deploy independentlyDeployments are already fast and safe
Parts of the app need different tech stacksYou can standardise on one stack
Build times are genuinely slowing you downYour build is under 5 minutes

The costs are real: more infrastructure, more operational surface area, harder debugging across bundle boundaries, potential for version skew between remotes, and inconsistent UX if teams don’t coordinate on the design system.

Alternatives Worth Considering

Before committing to MFE, check if simpler approaches solve the same problem:

When It’s Worth It

MFE is the right call when team autonomy at the deployment boundary is the core requirement — not just the build boundary. If teams truly need to ship independently, on their own cadence, without any coordination, then the runtime composition complexity is worth it.

If you’re doing it because the architecture looks impressive in diagrams, it’s not.

<< Previous Post

|

Next Post >>

#Architecture #Frontend #Javascript #Software-Engineering