Micro-Frontends at Two Scales: Module Federation at Work vs Multi-Zones for My Portfolio
I work with micro-frontends every day. At my day job, we run a federated architecture with Module Federation — multiple teams, independently deployed fragments, shared component libraries, the whole thing. For my portfolio, I wanted the same composability but with none of that operational weight.
The result: my portfolio is a shell that serves a live MTG deck analysis tool at /mtg-rag via Vercel Multi-Zones. Same concept, completely different trade-offs.
Module Federation: The Industrial Approach
Module Federation lets independently built and deployed applications share code at runtime. One app exposes a module; another consumes it. No npm packages, no rebuild-the-world deploys. Webpack (or Rspack) handles the wiring.
In practice, this looks like a shell application that loads remote fragments:
// Shell's module federation config
new ModuleFederationPlugin({
remotes: {
dashboard: 'dashboard@https://dashboard.cdn.example.com/remoteEntry.js',
settings: 'settings@https://settings.cdn.example.com/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
})Each remote is a separate build, separate deploy, separate team. The shell stitches them together at runtime.
Where It Shines
- Independent deployment. Team A ships without waiting for Team B. At scale, this is the killer feature — deploy velocity stays constant as the organisation grows.
- Runtime code sharing. Shared dependencies (React, your design system) load once. No duplication, no version conflicts if you enforce singletons.
- Granular composition. You can expose a single component, a whole page, or anything in between. The shell can compose fragments from multiple remotes on a single route.
Where It Hurts
- Operational complexity. Remote entry manifests, version negotiation, shared dependency graphs, fallback loading, error boundaries for when a remote is down. You need infrastructure to manage this.
- Debugging across boundaries. When something breaks at a module boundary, the stack traces are unhelpful. Source maps span builds. The error might be a version mismatch in a shared singleton that only manifests in production.
- Shared state is your problem. Module Federation shares code, not state. Cross-MFE communication — auth tokens, user context, theme — requires explicit contracts. We built a custom event bus and a shell-owned context provider to handle this.
- Build tooling lock-in. You're coupled to Webpack or Rspack. Moving to Turbopack or Vite means rethinking the federation layer entirely.
For a large engineering org with multiple teams shipping to the same product surface, Module Federation earns its complexity. The deploy independence alone justifies the overhead.
Vercel Multi-Zones: The Pragmatic Approach
Multi-Zones is Vercel's native take on micro-frontends. Each "zone" is a separate Next.js project with its own repo, build, and deployment. The shell defines path-based routing in a microfrontends.json config:
{
"applications": {
"portfolio": {
"development": {
"fallback": "portfolio.vercel.app"
}
},
"mtg-decklist-recommendations": {
"routing": [
{ "paths": ["/mtg-rag/:path*"] }
]
}
}
}That's the entire integration. No webpack plugins, no remote entry manifests, no shared dependency negotiation. Vercel's edge network routes /mtg-rag/* to the MTG app and everything else to the portfolio. Each app is a standard Next.js project that works independently.
Where It Shines
- Near-zero integration cost. A JSON config and a
basePathinnext.config.ts. Done. The MTG app runs standalone for local development and integrates seamlessly in production. - Full independence. Each zone is a complete Next.js app. Different dependencies, different versions, different everything. No shared dependency graphs to manage.
- Standard tooling. No plugins, no custom webpack config, no build-time coupling. Each app uses whatever Next.js supports natively.
- Free tier friendly. Vercel's free plan supports two projects with 50K requests. More than enough for a portfolio with a couple of live demos.
Where It Hurts
- No runtime code sharing. If both apps use React, both bundle React. There's no singleton negotiation — each zone is fully self-contained. For two apps this is fine; for twenty, the bandwidth cost adds up.
- Page-level granularity only. You can't embed a component from one zone inside another zone's page. Each route belongs to exactly one zone. This is a fundamental architectural constraint.
- Navigation is a full page load. Moving between zones triggers a hard navigation, not a client-side transition. The user briefly sees a loading state. For a portfolio where the MFE is a separate "app" experience, this is acceptable. For a seamless SPA feel, it's not.
- Shared chrome requires duplication. Each zone renders its own layout. I built a matching header in the MTG app that links back to the portfolio — it looks cohesive but it's duplicated code, not shared code.
The Trade-offs, Side by Side
| Concern | Module Federation | Multi-Zones | |---|---|---| | Integration cost | High (webpack config, manifests, shared deps) | Low (JSON config, basePath) | | Deploy independence | Full | Full | | Runtime code sharing | Yes (singletons) | No (each zone self-contained) | | Composition granularity | Component-level | Page-level | | Navigation | Client-side (SPA feel) | Hard navigation between zones | | Shared state | Custom solution needed | Custom solution needed | | Build tooling | Webpack/Rspack only | Any (standard Next.js) | | Operational overhead | Significant | Minimal |
Theme Sync: The Same Problem at Both Scales
One thing both approaches share: the shell and its fragments need to agree on shared state. Auth, user preferences, theme.
With Module Federation, we solved this with a shell-owned React context and a custom event bus. The shell provides the context; remotes consume it. It works, but it's a meaningful piece of infrastructure to build and maintain.
With Multi-Zones, there's no shared React tree. Each zone is a separate app. I solved theme sync with localStorage — the portfolio writes the user's theme preference, and the MTG app reads it on mount:
// ThemeSync component in the MTG app
useEffect(() => {
const stored = localStorage.getItem("theme");
if (stored === "light") {
document.documentElement.classList.remove("dark");
}
window.addEventListener("storage", (e) => {
if (e.key === "theme") {
// React to theme changes from the portfolio
}
});
}, []);Same problem. Simpler solution. The key difference: Multi-Zones are on the same origin (same domain, shared localStorage), so cross-app state sharing comes free via the browser. Module Federation remotes might be on different CDN origins, making this harder.
Choosing the Right Scale
Module Federation is the right choice when:
- Multiple teams ship to the same product surface
- You need component-level composition (embedding fragments within pages)
- Deploy independence at scale is a hard requirement
- You can invest in the operational infrastructure
Multi-Zones are the right choice when:
- You want independently deployed apps behind a unified domain
- Page-level routing is sufficient
- You value simplicity and standard tooling
- The project count is small (single digit)
For my portfolio, Multi-Zones took about an hour to set up. The MTG app works standalone for development, deploys independently, and serves through the portfolio at /mtg-rag. Adding another project MFE means creating a new Vercel project and adding three lines to the JSON config.
The architecture you choose should match the problem you're solving, not the one you wish you had.