Micro-frontend Architecture: When it Makes Sense and How to Implement it
Micro-frontends aren't just about splitting code; they are about decoupling team lifecycles. Learn when to adopt them, how to use Module Federation 3.0, and the hard lessons I learned from scaling to 15+ independent apps.

The Monolith Ceiling
Your CI/CD pipeline just hit 42 minutes for a single-line CSS change. Three different squads are fighting over who owns the navigation component in a 4,000-line App.tsx file. You’re not just dealing with technical debt; you’ve hit the 'Monolith Ceiling' where the cost of coordination exceeds the value of the code produced. In my experience, this is the exact moment when micro-frontend (MFE) architecture stops being a 'cool experiment' and starts being a survival requirement.
Micro-frontends are not a performance optimization. In fact, if done poorly, they will make your site slower. They are a human scaling optimization. By the end of 2025, the industry moved past the 'iframe vs. AJAX' debate and settled on Module Federation as the gold standard. But even with the best tools, most teams fail because they treat MFEs like a technical problem rather than an organizational one.
When Does it Actually Make Sense?
Don't let the hype fool you. If you have a team of 5 developers, you do not need micro-frontends. You need a better folder structure. I’ve seen teams of 10 waste six months building an MFE orchestrator only to realize they could have just used a monorepo with Turborepo or Nx.
Adopt micro-frontends only when:
- Independent Deployment is Critical: Team A (Billing) needs to deploy five times a day, while Team B (Compliance) only deploys once a month. In a monolith, Team B’s slow QA process blocks Team A’s features.
- Technology Heterogeneity is Required: You are migrating a legacy Vue 2 app to React 19, and you can't afford a full rewrite. You need to run them side-by-side during the transition.
- Team Autonomy: You have 30+ developers split into 'Domain Squads.' Each squad should own their stack from the database to the UI without asking for permission to change a dependency version.
Implementing with Module Federation 3.0 and Rspack
In 2026, we've moved away from Webpack. It's too slow for the scale of MFEs. We use Rspack—the Rust-based alternative—paired with Module Federation 3.0. This combination gives us sub-second HMR (Hot Module Replacement) even when pulling in five remote apps.
Here is a production-ready configuration for a 'Host' application that consumes a 'Remote' billing module. Note the use of runtimePlugins for advanced features like version negotiation.
// rspack.config.ts
import { defineConfig } from '@rspack/cli';
import { ModuleFederationPluginV3 } from '@module-federation/sdk/rspack';
export default defineConfig({
output: {
publicPath: 'http://localhost:3000/',
},
plugins: [
new ModuleFederationPluginV3({
name: 'host_app',
remotes: {
billing: 'billing@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
'@tanstack/react-query': { singleton: true },
},
// 2026 feature: Advanced runtime orchestration
runtimePlugins: [require.resolve('./plugins/custom-error-boundary')],
}),
],
});
The Type-Safety Problem
The biggest pain point used to be losing TypeScript types across the network boundary. We solved this using @module-federation/native-federation-typescript. It automatically generates and syncs .d.ts files from your remotes to your host during the build process.
Runtime Communication: The Event Bus
Sharing state between MFEs is where most architects lose their way. Do not—under any circumstances—share a single Redux store across all micro-frontends. This creates a 'Distributed Monolith' where a change in the 'Cart' state breaks the 'Header' remote.
Instead, use a Typed Event Bus or Custom Events. This keeps the apps decoupled. If the Billing MFE isn't loaded, the events simply fire into the void, and nothing crashes.
// packages/shared-events/index.ts
export enum MfeEvents {
USER_LOGGED_IN = 'user:logged_in',
CART_UPDATED = 'cart:updated',
}
export interface UserPayload {
id: string;
token: string;
}
export const dispatchMfeEvent = <T>(eventName: MfeEvents, detail: T) => {
const event = new CustomEvent(eventName, { detail, bubbles: true });
window.dispatchEvent(event);
};
// Usage in Remote App (Billing)
useEffect(() => {
const handler = (e: Event) => {
const { id } = (e as CustomEvent<UserPayload>).detail;
console.log(`Billing received user: ${id}`);
};
window.addEventListener(MfeEvents.USER_LOGGED_IN, handler);
return () => window.removeEventListener(MfeEvents.USER_LOGGED_IN, handler);
}, []);
What Went Wrong: Lessons from the Trenches
I once led a migration for a fintech dashboard where we split the app into 12 remotes. Within a month, our 'Shared Components' library became the new bottleneck. Every team wanted to add a small prop to the Button component, leading to 14 breaking changes in two weeks.
The Lesson: Avoid a massive 'Shared UI' library. Instead, provide a Design System (CSS/Tailwind tokens) and let teams implement their own components. If you must share components, they should be extremely stable and versioned properly via NPM, not shared via Module Federation.
Another disaster: CSS Leakage. We had a remote app using an old version of Bootstrap that leaked styles into the host's Tailwind layout. In 2026, the answer is non-negotiable: Shadow DOM encapsulation or strict CSS Modules. Don't trust developers to 'just use unique class names.'
Common Gotchas the Docs Don't Tell You
- The Singleton Trap: If you mark
reactas a singleton in your config, but Team A uses version19.0.0and Team B uses18.2.0, Module Federation will throw a runtime warning or error. You need a strict policy on 'Core Dependency' alignment. - Zombie Remotes: When you take a remote server down for maintenance, the host will crash if you haven't implemented a proper
ErrorBoundaryaround yourReact.lazyimports. - SEO and SSR: Module Federation with SSR is still complex. If SEO is your top priority (e.g., an e-commerce landing page), stick to a monolith or use Next.js Multi-zones instead of pure Module Federation.
Takeaway
Micro-frontends are an organizational tool, not a performance one. If you're ready to scale, start with a single vertical slice. Take one non-critical part of your app (like the 'Settings' page), extract it into a remote using Rspack and Module Federation 3.0, and deploy it independently. Only once you've automated the type-sharing and deployment pipeline for that one slice should you even think about splitting the rest of the app.