Micro frontends answer a question most teams never have to ask: how do several independent teams ship features into one user facing application, on independent schedules, without coordinating releases or stepping on each other's code. When that question is real, the pattern and its webpack era enabler, Module Federation, are a serious answer. When the question is not real, one team with one product adopting micro frontends buys the coordination costs of a distributed system, version contracts, shared dependency governance and integration testing across independently deployed surfaces, without the independence that justifies them. This article builds the honest case: what Module Federation actually does, the architecture it enables, the costs it always charges, the integration patterns that survive production, and the scale test that decides whether a codebase should ever open this door.

What Module Federation actually moves across build boundaries

Module Federation lets one JavaScript bundle expose modules at runtime and another bundle consume them, with the two built and deployed separately. A federated remote declares what it shares:

// catalog app, webpack or rspack config
new ModuleFederationPlugin({
  name: "catalog",
  filename: "remoteEntry.js",
  exposes: {
    "./ProductList": "./src/components/ProductList.jsx"
  },
  shared: { react: { singleton: true }, "react-dom": { singleton: true } }
})

and the host loads the remoteEntry at a configured URL and imports the exposed module as if it were local. The runtime resolves the network boundary, the shared clause deduplicates common dependencies so the host's single React instance renders remote components without a second React in the page, and the remote deploys on its own cadence with the host picking up the new version on its next load.

The single most important word in the configuration is singleton. Two copies of a framework like React in one page break the invariants frameworks depend on, context providers, event systems and reconciler state, and the shared dependency mechanism exists to prevent that. The second most important consequence is the contract: the exposed name and the interface behind it. The host consumes a version of that interface, the remote evolves it, and the pair needs the same compatibility discipline as any published library, semver thinking, deprecation windows and a changelog, applied between two parts of the same web page.

The independence that justifies the pattern

The justification is organizational before it is technical. A product with four or more feature teams, each owning a vertical slice, catalog, checkout, account, and support, each with its own backlog, release rhythm and on call rotation, meets coordination friction everywhere in a single codebase: the shared repository queues every team's merge behind every other team's review, the shared release train waits for the slowest passenger, and a refactor inside one slice is reviewed by people who do not own it. Micro frontends cut those queues by cutting the repository boundary, and the deployment boundary that follows it. The catalog team ships Tuesday because Tuesday is ready, and the checkout page does not retest.

The technical gains follow the organizational ones. Independent deploys mean smaller blast radius, an incident in the checkout remote leaves the catalog serving, and rollbacks are scoped to the slice that regressed. Team owned technology choices become possible within limits, one slice can adopt a new state library or even, with care, a different framework version boundary, without a cross product negotiation. These gains are exactly proportional to the number of teams that actually owns slices, which is the origin of the scale test this article returns to at the end.

The costs the pattern always charges

The independence is paid for in four currencies. The first is the dependency contract: shared singletons must stay version compatible across surfaces that deploy independently, which means the team operating the host usually publishes the approved versions, and a remote that drifts breaks at integration, not at build. The second is the integration surface: the page the user sees is assembled from independently deployed parts, and the seams, the layout boundaries, the event and data handoffs between slices, are where defects appear that no single team's tests catch. Cross slice user journeys need their own end to end coverage against the assembled page, run by the team that owns the shell.

The third is the operational footprint: every remote is a deployable unit with a pipeline, an environment matrix and a registry entry, and the platform investment that one monolith spent on one pipeline now multiplies by the number of slices. The fourth is latency and failure: a remote entry that fails to load takes its slice with it unless the host wraps the consumption in error boundaries and loading states, and the network waterfall of remote entries wants a load strategy, eager for critical slices and lazy for everything below the fold. None of these costs are exotic, they are the ordinary costs of any distributed system, arriving at the frontend for the same reason they arrived at the backend: because independent parts are harder to assemble than one part is to build.

Integration patterns that survive production

The successful assemblies converge on a small set of patterns. The shell owns layout, navigation and session, and slices render into designated regions rather than owning whole pages where possible, which keeps the visual contract in one place. Navigation between slices goes through the shell's router with route based loading, each route lazily importing the remote that serves it, so the federation boundary follows the route boundary that follows the team boundary. Shared state is deliberately minimal: session identity, feature flags, and a small event bus for loose notifications, while anything a slice owns stays inside it, and the temptation to share a store is treated as a design smell, because a shared store is a shared codebase with extra steps.

The version strategy deserves one explicit decision. The simplest robust pattern is the host pinning remote URLs with an indirection, a small config service or a version manifest the host reads at load, so a bad remote version is rolled back by repointing the manifest, without redeploying anyone. Error boundaries wrap every remote slot with a fallback region, so one failing slice degrades instead of taking the page. And the load order treats remoteEntry scripts as critical for above the fold routes and lazy below it, measured with the field data rather than the lab, because the assembly cost is real and belongs in the performance budget of the route.

The shell in practice with routes and lazy slots

The host side of a working assembly is small, which is part of the pattern's charm. The shell declares the remotes it may consume, by URL or by manifest lookup, and the routes lazily import the exposed modules:

// host webpack config
new ModuleFederationPlugin({
  name: "shell",
  remotes: {
    catalog: "catalog@https://catalog.cdn.example/remoteEntry.js",
    checkout: "checkout@https://checkout.cdn.example/remoteEntry.js"
  },
  shared: { react: { singleton: true }, "react-dom": { singleton: true } }
})
// shell router
const ProductList = lazy(() => import("catalog/ProductList"))
const Cart = lazy(() => import("checkout/Cart"))

<Route path="/products" element={
  <Suspense fallback={<SlotSkeleton />}>
    <ProductList session={session} />
  </Suspense>
} />

Two details in that snippet carry the production weight. The Suspense boundary is where the load strategy lives, eager import for the routes users hit first, lazy for the rest, and the skeleton keeps the layout stable while the remote arrives. The session prop is the whole shared state story in miniature: the shell owns identity and hands it down as data, the remote renders it, and nothing upstream flows back except through events the shell subscribed to deliberately. A shell that stays this thin is the difference between an assembly that operators can reason about and one that becomes its own monolith, a shared codebase with extra network hops, which is the failure mode the pattern's critics correctly warn about.

The scale test that keeps teams honest

The pattern's honest adoption test is a question about teams, not about code: can you name three or more teams that own vertical slices of one application and ship them on their own cadences. If yes, the coordination the pattern removes is already visible in the release calendars and the review queues, and the costs above buy real autonomy back. If the answer is one team, or two teams with one backlog, the pattern taxes the product with its full costs while its benefits are unavailable, because there is no independence to enable, only infrastructure to operate.

The middle cases deserve honesty too. Two teams can usually get coordination relief from a monorepo with clear ownership boundaries and path based code review rules, without the deployment machinery. A single team feeling build pain usually needs build caching and better boundaries, not a distributed frontend. The refactor that introduces federation before the organization has the teams is the same rewrite instinct this series has met twice already, in the Vue migration and the styling migration, arriving one more time in a new costume, and it fails for the same reason: it changes the architecture before changing the thing the architecture exists to serve.

The adoption checklist for a federation proposal

A proposal to federate a frontend survives review when it answers the operational questions in advance, and the list is short:

  1. Name the owning teams per slice, with their release cadences, before naming the technology;
  2. Publish the shared dependency contract, the approved versions and the upgrade process;
  3. Design the seam first, routes, layout regions and the minimal shared state, and review it like an API;
  4. Wrap every remote slot in an error boundary and a loading region from the first integration;
  5. Pin remote versions through a manifest the host reads, so rollback is a config change;
  6. Fund the cross slice end to end tests and the platform pipelines before the first slice ships.

Federation is a serious answer to a real organizational problem, and like every serious distributed system it rewards the teams that arrive with the contract drawn and punishes the ones that arrive with only the enthusiasm. The scale test is free, it costs one honest conversation, and it is the cheapest way to avoid the most expensive mistake in the pattern's entire playbook.

Testing the seams without assembling the fleet

The integration surface is where federation breaks when it breaks, so the tests concentrate there, and the shape is a contract test pair. The remote publishes its contract as a test fixture, the exposed names, the props each surface accepts, the events each surface emits, and the contract test runs in the remote's own pipeline against its own build, failing the build when a change breaks the published shape. The host consumes the same contract as a mock, a generated stub that implements the current published version, and the shell's pipeline runs its integration tests against the mocked remotes, so the host fails its own build when it depends on something the contract does not contain.

The pair catches the dangerous class of defect, the remote that renamed a prop and shipped, the host that called a method that never existed, at build time in two pipelines instead of runtime on one user's screen. The assembled page still gets end to end tests, this article's funding rule from the cost section, but the assembly tests run the critical journeys only, the login, the browse, the buy, because the seams are already covered by the contract pair and the assembly suite exists to catch what contracts cannot, layout, navigation and the session handoff. The testing pyramid inverts the usual argument for federation, because the independent deployments that make the architecture attractive are exactly what make the contract tests non negotiable, and the team that ships a remote without a contract test is shipping a dependency that its consumers can only verify in production.