Case study · Plate ii-A · Frontend Architecture

Study ref. FE-01

Micro-Frontend Migration

Component Composition & Module Federation at Scale

Context SaaS platform, 6 product teams
Scale ~180k lines of JSX, 340 components
Duration 12 weeks (rolling extraction)
Role Lead Solution Architect
I — Context & Problem Statement

The Situation

A B2B SaaS platform had grown from a single-team startup into a six-team product organisation — analytics, payments, settings, notifications, marketplace, and a shared design system — all shipping from one monolithic React application. Every release required a full-suite regression pass and coordination across all six teams to merge a shared release branch.

The consequence was a two-week release cycle that served no team well. A critical payments bug fix sat blocked for nine days waiting for an analytics feature freeze. The notification team's bundle changes broke marketplace routing twice in a quarter. Prop drilling was endemic: user session data threaded through eleven component layers to reach a deeply nested billing widget that needed only the account tier.

Constraints

  • No user-visible downtime during migration — feature development continues in parallel
  • Shared design system must remain the single source of truth across all micro-frontends
  • Teams own their deployment pipelines; no central release coordinator
  • Existing React Router v5 routing layer must be preserved until all MFEs are extracted
II — Architecture Diagram · Before & After

System Evolution

Left: monolithic React application where all six domains share one build, one router, and one deployment pipeline. Right: module federation target state where each domain MFE deploys independently and the shell orchestrates routing and shared context.

Before — Monolithic React App
REACT MONOLITH · ONE BUILD · ONE DEPLOY SHARED ROUTER React Router v5 (global) ANALYTICS 62 components PAYMENTS 48 components SETTINGS 35 components NOTIFICATIONS 29 components MARKETPLACE 54 components DESIGN SYS shared (duplicated) PROP DRILLING · 11 LAYERS DEEP SESSION DATA PASSED EVERYWHERE ONE BUILD · 14-DAY RELEASE CYCLE
After — Module Federation + Shell
SHELL APP router · auth context · layout DESIGN SYSTEM singleton · npm package ANALYTICS MFE own CI/CD PAYMENTS MFE own CI/CD SETTINGS MFE own CI/CD NOTIFS MFE own CI/CD MARKET MFE own CI/CD AUTH CONTEXT · SHARED ONCE VIA SHELL no prop drilling — context injected at shell boundary INDEPENDENT DEPLOYS · NO SHARED BRANCH DESIGN SYSTEM IS SINGLETON · NOT DUPLICATED WEBPACK MODULE FEDERATION · RUNTIME SHARING

The critical insight: the shell owns routing and authentication context. Each MFE receives what it needs through a typed contract at the module boundary — not through eleven layers of prop threading. The design system is loaded once at the shell level and shared via Webpack Module Federation's singleton scope, eliminating duplicate React instances entirely.

III — Approach & Key Decisions

Migration Strategy: Strangler Fig per Domain

Extraction followed domain ownership — the payments team migrated first because they had the clearest bounded context and the highest pain from the shared release cycle. Each domain was extracted as a standalone Webpack 5 build, exposed via Module Federation's exposes configuration, and consumed by the shell via a remote entry point.

During the twelve-week migration, the monolith and MFEs coexisted behind a feature flag at the shell routing layer. Payments could deploy on Tuesday; the monolith's remaining domains deployed on the old fortnightly cycle. Zero coordination required for independent domains.

Client requests pass through a DNS weight gate at the shell boundary; the MFE share climbs from 5% to 100% over each domain's rollout window while the monolith continues serving the remainder — rollback is a single weight change, not a redeploy.

Eliminating Prop Drilling

The session object had been threaded through eleven component layers because the monolith had no clean boundary at which to inject it. The shell introduced a typed AuthContext published once at the root. Each MFE consumed it directly with useAuth() — the eleven intermediate layers simply stopped carrying the prop.

Where state needed to cross MFE boundaries (e.g. a payments event visible to the notifications MFE), a lightweight event bus was introduced at the shell level. No shared global store — only typed, versioned events flowing in one direction.

Design System Contract

The existing shared component library was promoted to a versioned npm package. Module Federation's singleton: true flag ensured only one React instance ran in the browser regardless of how many MFEs loaded. Breaking changes required a semver major bump and a shell-level compatibility matrix review — enforced in CI.

Each MFE declared the design system as a peer dependency, not a bundled dependency. The shell owned the resolved version. This eliminated the "double React" problem that had caused hook invariant failures in early proof-of-concept work.

Team Enablement

A contract-first rule governed cross-MFE integration: a team could change anything inside their MFE boundary freely, but changes to the shell contract or shared event schema required an RFC reviewed by all affected teams. This kept teams autonomous without creating invisible coupling — the same principle that hexagonal architecture applies at the backend boundary.

IV — Results & Reflection
on-demand

Release cadence per MFE

Down from a mandatory 14-day shared release cycle

74%

Fewer cross-team merge conflicts

Each team owns its own repository and branch strategy

3×

Faster local dev startup

Engineers run only their MFE against a stubbed shell

0

User-visible regressions

Feature flags and shadow routing prevented any downtime

What I Would Do Differently

The event bus — introduced late as an afterthought for cross-MFE communication — became a source of invisible coupling within three months. Teams added event types without a schema registry, and by month four, the payments MFE was listening to events from six other teams with no versioning contract. A typed event schema registry, enforced in CI from day one, would have prevented this.

The extraction order followed team willingness rather than dependency topology. Analytics, which many other MFEs linked into for instrumentation, was extracted last — meaning for ten weeks, the analytics package was imported from the monolith into newly extracted MFEs via a re-export shim. The shim worked but added complexity that could have been avoided by extracting shared infrastructure MFEs first.