Senior Frontend Interview Guide, Part 4: Micro Frontends and Module Federation
Large enterprises rarely have one frontend team. A bank's online platform might have separate teams for accounts, payments, cards, lending, onboarding, and settings, each with its own roadmap and release schedule. When all of them commit to one monolithic single-page application, builds slow down…

This article is Part 4 of 18 in The Senior Frontend Engineer Interview Guide, and includes 22 interview questions with model answers.
Large enterprises rarely have one frontend team. A bank’s online platform might have separate teams for accounts, payments, cards, lending, onboarding, and settings, each with its own roadmap and release schedule. When all of them commit to one monolithic single-page application, builds slow down, releases require coordination across teams, and one team’s bug blocks everyone’s deployment. Micro frontends apply the ideas of microservices to the browser: split the UI into independently built and deployed pieces owned by autonomous teams. Module Federation is the most common way to stitch those pieces together at runtime.
What a Micro Frontend Is
A micro frontend is a portion of a web application, usually aligned with a business domain, that is:
- Owned by one team end to end, often including its backend-for-frontend.
- Independently deployable: shipping it does not require redeploying the rest.
- Technology-isolated to a degree: its internals can change without other teams knowing.
- Composed with other micro frontends into a single user experience by a host (also called a shell or container).
The goal is organizational scalability, not technical novelty. Interviewers like candidates who say this explicitly: micro frontends trade runtime and operational complexity for team autonomy, and they are worth it only when the organization is large enough to feel the pain of a monolith.
When Not to Use Micro Frontends
- A small team (fewer than three or four squads) working on one product.
- An application whose features are tightly coupled and change together.
- When the real problem is code quality or build speed that a well-structured monorepo would fix.
- When the organization cannot invest in a platform team, shared design system, and governance.
A modular monolith (one deployable app with strict internal boundaries enforced by tooling such as Nx module boundaries) is often the right first step.
Ways to Split
| Split | Example | Notes |
|---|---|---|
| Horizontal (by page/route) | /payments/* belongs to Payments, /cards/* to Cards |
Simplest; one micro frontend per route |
| Vertical within a page | Header, account summary widget, offers widget on the dashboard | More composition complexity, more sharing |
| By domain capability | “Payee management” used inside several flows | Needs a stable contract |
Domain-driven design helps: align micro frontends with bounded contexts so that each team’s language and data model are internally consistent.
Composition Approaches
Micro frontends can be composed at build time, on the server, at the edge, or in the browser.
| Approach | How | Pros | Cons |
|---|---|---|---|
| Build-time (npm packages) | Each MFE published as a package; host depends on versions | Simple, type-safe, optimized bundle | Not independently deployable: host must rebuild to update |
| Server-side includes / edge composition | Server or CDN stitches HTML fragments (SSI, ESI, Podium, Tailor) | Fast first paint, SEO | Interactivity and shared state harder |
| iframes | Each MFE in its own iframe | Strong isolation (security, CSS, JS) | Poor UX: sizing, routing, accessibility, performance |
| Web Components | Each MFE exposes a custom element | Framework-agnostic contract | Shadow DOM styling and SSR complexity; React 18 has limited custom-element prop support |
| Runtime JS (Module Federation, single-spa, import maps) | Host loads remote JavaScript at runtime | Independent deploys, shared deps, good UX | Version coordination, runtime failures, more ops |
Most React enterprise platforms choose runtime composition with Module Federation, sometimes combined with single-spa for lifecycle management or with Web Components as an integration contract for non-React teams.
Module Federation Fundamentals
Module Federation was introduced in webpack 5 (2020). It allows a JavaScript application to load code from another, separately built and deployed application at runtime, and to share dependencies between them.
Core concepts:
- Host (consumer): the application that loads remote code. Usually the shell.
- Remote (producer): an application that exposes modules for others to consume.
- Exposes: the modules a remote makes available, such as
./PaymentsApp. - Remotes: the list of remotes a host knows about, mapped to URLs of their
remoteEntry.jsfiles. - Shared: dependencies (React, React DOM, the design system, the router) that should be loaded once and shared, with version negotiation.
- Container: the runtime object a remote’s
remoteEntry.jsregisters, withinit(shareScope)andget(moduleName)methods.
An application can be both a host and a remote (“bidirectional” or “omnidirectional” federation).
Remote Configuration
// payments/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
module.exports = {
output: { publicPath: 'auto', uniqueName: 'payments' },
plugins: [
new ModuleFederationPlugin({
name: 'payments',
filename: 'remoteEntry.js',
exposes: {
'./PaymentsApp': './src/bootstrap/PaymentsApp',
'./QuickTransferWidget': './src/widgets/QuickTransfer',
},
shared: {
react: { singleton: true, requiredVersion: deps.react },
'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
'react-router-dom': { singleton: true, requiredVersion: deps['react-router-dom'] },
'@bank/design-system': { singleton: true, requiredVersion: '^4.0.0' },
zustand: { singleton: false, requiredVersion: deps.zustand },
},
}),
],
};
Host Configuration
// shell/webpack.config.js
new ModuleFederationPlugin({
name: 'shell',
remotes: {
payments: 'payments@https://cdn.bank.example/payments/remoteEntry.js',
cards: 'cards@https://cdn.bank.example/cards/remoteEntry.js',
},
shared: {
react: { singleton: true, eager: false, requiredVersion: deps.react },
'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
'react-router-dom': { singleton: true, requiredVersion: deps['react-router-dom'] },
'@bank/design-system': { singleton: true, requiredVersion: '^4.0.0' },
},
});
// shell/src/routes.jsx
const PaymentsApp = React.lazy(() => import('payments/PaymentsApp'));
<Route path="/payments/*" element={
<RemoteBoundary name="payments">
<Suspense fallback={<PageSkeleton />}>
<PaymentsApp basePath="/payments" />
</Suspense>
</RemoteBoundary>
} />
The Async Boundary (Bootstrap) Pattern
Shared modules are negotiated asynchronously before any code that uses them runs. If the entry file imports React synchronously, webpack throws “Shared module is not available for eager consumption.” The standard fix is an asynchronous boundary:
// src/index.js
import('./bootstrap');
// src/bootstrap.js
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')).render(<App />);
The dynamic import() gives the federation runtime a chance to initialize the share scope before React is imported. The alternative, marking a shared module as eager: true, bundles it into the initial chunk, which increases bundle size and defeats sharing; it is mainly used in the host for critical dependencies.
Shared Dependency Options
| Option | Meaning |
|---|---|
singleton: true |
Only one version may be loaded across all apps. Essential for React, React DOM, router, and anything holding global state or context. |
requiredVersion |
Semver range this app needs. With a singleton, a mismatch produces a warning (or an error with strictVersion). |
strictVersion: true |
Throw instead of warning when no compatible version is available; then the fallback is used if not a singleton. |
eager: true |
Include in the initial chunk rather than loading asynchronously. |
shareScope |
Name of the share scope; default is default. Useful to isolate groups. |
import: false |
Do not provide a fallback; rely entirely on the host’s shared version. |
How negotiation works: each container registers the versions of shared modules it can provide into the share scope. When a module is requested, the runtime picks the highest version that satisfies the requested range (for singletons, the single loaded version). If no compatible version exists and the dependency is not a singleton, the consumer uses its own bundled fallback.
React must be a singleton. Two copies of React lead to the classic “Invalid hook call” error, because hooks rely on a module-level dispatcher, and a component rendered by one copy of React will look for the dispatcher in the other.
Dynamic Remotes
Hard-coding remote URLs at build time ties the host to specific environments. Enterprises usually load remotes dynamically from a manifest or service discovery endpoint so the same host build can run in dev, QA, staging, and production and so remotes can be rolled back by changing a pointer.
// Loading a remote at runtime with the webpack federation runtime API
async function loadRemote({ url, scope, module }) {
await __webpack_init_sharing__('default');
await loadScript(url); // injects <script src=url>
const container = window[scope];
await container.init(__webpack_share_scopes__.default);
const factory = await container.get(module);
return factory();
}
const manifest = await fetch('/mfe-manifest.json').then(r => r.json());
// { "payments": { "url": "https://cdn.../payments/1.42.0/remoteEntry.js" } }
Module Federation 2.0 (the @module-federation/enhanced runtime, maintained by the module-federation organization and integrated into Rspack and Rsbuild) provides a cleaner runtime API (init, loadRemote, registerRemotes), a manifest format (mf-manifest.json), runtime plugins for retry and logging, Chrome DevTools support, and automatic TypeScript type sharing between remotes and hosts. Vite projects use @originjs/vite-plugin-federation or the module-federation Vite plugin. Rspack offers webpack-compatible federation with much faster builds.
Routing Across Micro Frontends
A common model:
- The shell owns the top-level router and maps path prefixes to remotes.
- Each remote owns nested routes under its prefix and receives
basePathas a prop or uses a relative router. - The router library must be a shared singleton so that both see the same history and context.
- Cross-MFE navigation uses URLs, not imports: Payments navigates to
/cards/123, never imports a Cards component.
Avoid two routers listening to the same history independently, which leads to double renders and conflicting state.
Communication Between Micro Frontends
The guiding principle is loose coupling: micro frontends should know as little about each other as possible.
Options, from loosest to tightest:
- URL and query parameters for navigation state.
- Browser events (
CustomEventonwindow) or a tiny pub/sub event bus provided by the shell, with versioned, documented event contracts. - Props passed from the host to the mounted remote, such as
user,locale,featureFlags,onNavigate. - Shared services exposed by the shell: auth client, analytics, logger, feature flag client.
- Shared global store, which creates strong coupling and should be avoided except for a minimal, stable slice (current user, locale).
// shell/src/eventBus.js
const target = new EventTarget();
export const bus = {
publish(type, detail) { target.dispatchEvent(new CustomEvent(type, { detail })); },
subscribe(type, handler) {
const listener = e => handler(e.detail);
target.addEventListener(type, listener);
return () => target.removeEventListener(type, listener);
},
};
// payments remote
bus.publish('payments.transfer.completed.v1', { accountId, amountCents });
// accounts remote
useEffect(() => bus.subscribe('payments.transfer.completed.v1', () => refetchBalances()), []);
Treat events like an API: version them, document their payload schema, and never put secrets or full PII in them.
Shared Concerns
Authentication
The shell handles login and token acquisition (Chapter 9) and exposes an auth client to remotes, for example getAccessToken(). Remotes must not implement their own login. Tokens should not be broadcast through events. Each remote’s API calls go through a shared HTTP client or receive a token-provider function.
Design System
A shared component library ensures visual consistency. Share it as a singleton through federation, or as a versioned npm package if teams need to upgrade independently. Use design tokens (CSS custom properties) so themes apply across all remotes.
CSS Isolation
Global CSS from one micro frontend can break another. Strategies:
- CSS Modules or CSS-in-JS to scope class names.
- A required class prefix per micro frontend (
.pay-), enforced by Stylelint. - Shadow DOM when using Web Components (strong isolation, but harder theming).
- Never style bare element selectors (
button { ... }) outside the shell’s reset. - CSS cascade layers (
@layer reset, design-system, app;) to control precedence.
Observability
Each remote should report errors and performance metrics tagged with its name and version. Errors that cross boundaries (host calling remote) should include both. Use a shared logger exposed by the shell, and propagate correlation IDs to backend calls.
Resilience
A remote can fail to load (CDN outage, deploy mistake, network error) or throw at runtime. The host must survive:
class RemoteBoundary extends React.Component {
state = { failed: false };
static getDerivedStateFromError() { return { failed: true }; }
componentDidCatch(error) {
logger.error('remote failed', { remote: this.props.name, error });
}
render() {
if (this.state.failed) {
return (
<section role="alert">
<h2>This section is temporarily unavailable</h2>
<button onClick={() => window.location.reload()}>Try again</button>
</section>
);
}
return this.props.children;
}
}
Additional techniques: timeouts and retries when loading remoteEntry.js, fallbacks to a previous known-good version, feature flags to disable a remote, and health checks in the manifest service.
Versioning, Deployment, and Contracts
- Immutable, versioned assets. Deploy each remote to a versioned path (
/payments/1.42.0/) with content-hashed filenames, and set long cache lifetimes.remoteEntry.js(or the manifest) gets a short cache lifetime or is resolved via the manifest service. - Manifest-based releases. Releasing a remote is changing its pointer in the manifest; rolling back is pointing it back. This also enables canary releases (a percentage of users get the new version).
- Contracts. The exposed module’s props, events, and shared dependency ranges form a contract. Version it semantically and test it with contract tests: the host’s expectations of the remote and vice versa.
- Independent pipelines. Each remote has its own CI/CD pipeline: lint, unit tests, build, publish assets, run integration tests against the current production host, then update the manifest.
- Integration environment. Run end-to-end tests that compose the latest versions of all remotes before promotion to production.
Performance Considerations
- Every remote adds at least one network request for its entry file plus its chunks. Preload
remoteEntry.jsfor likely routes with<link rel="preload">or by triggering the load on hover. - Share large dependencies, but be careful: sharing too many small libraries adds negotiation overhead and couples teams to version ranges.
- Avoid duplicate copies of large libraries due to incompatible ranges; monitor bundles per remote.
- Server-side rendering with federation is possible (Next.js and Modern.js have federation support) but significantly more complex; many enterprise dashboards behind login use client-side rendering with a fast shell.
single-spa and Alternatives
single-spa is a framework that manages the lifecycle of micro frontends (bootstrap, mount, unmount) based on routes and can host apps written in different frameworks. It is often combined with Module Federation or import maps for loading. Import maps (natively supported in modern browsers) map bare specifiers like react to URLs, enabling a similar shared-dependency model without a bundler plugin. Piral, Luigi, and Qiankun are other frameworks. Native Federation applies federation concepts using import maps and ES modules, independent of webpack.
Testing Micro Frontends
- Unit and component tests within each remote, mocking the shell-provided services.
- Contract tests for exposed modules and events (for example, a schema test that every event a remote publishes matches its documented JSON schema).
- Standalone mode: each remote can run on its own with a mock shell for development and testing.
- Integration and end-to-end tests (Playwright) against a composed environment, covering cross-MFE flows such as “make a transfer and see the balance update on the accounts page”.
Interview Questions and Answers
Q1. What are micro frontends, and what problem do they solve?
An architectural style that splits a frontend into independently developed and deployed pieces, each owned by a team aligned to a business domain, composed into one application. It solves organizational scaling problems: slow monolithic builds, release coordination across many teams, and unclear ownership. It is not primarily a performance technique and adds complexity, so it suits large organizations.
Q2. Explain Module Federation.
A webpack 5 feature (now also in Rspack and via plugins for Vite) that lets one build load modules from another build at runtime. Remotes expose modules through a remoteEntry.js container; hosts declare remotes and import from them like normal modules. Shared dependencies are negotiated at runtime so libraries like React are loaded once.
Q3. Why must React be a singleton in a federated app?
Hooks depend on a module-level dispatcher in React. If a component is rendered by one copy of React but calls hooks from another copy, the dispatcher is null, causing “Invalid hook call”. Context also breaks across copies. Marking React and React DOM as singleton: true ensures every app uses the same instance.
Q4. What does “Shared module is not available for eager consumption” mean, and how do you fix it?
The app tried to use a shared module synchronously before the federation runtime negotiated the share scope. Fix it by adding an async boundary: the entry file does import('./bootstrap') and all app code lives in bootstrap. Alternatively mark the dependency eager: true, which embeds it in the initial chunk at the cost of bundle size.
Q5. How do micro frontends communicate?
Prefer loose coupling: URLs for navigation state, custom events or a shell-provided event bus with versioned contracts for notifications, and props from the host for context such as user and locale. Shared services from the shell handle cross-cutting concerns like auth and analytics. A shared global store is a last resort because it couples teams.
Q6. How do you handle routing?
The shell owns the top-level router and maps path prefixes to remotes; each remote handles nested routes under its base path, sharing the same router instance as a singleton. Remotes navigate to each other via URLs rather than importing each other’s components.
Q7. How do you handle a remote that fails to load?
Wrap each remote in an error boundary and Suspense with meaningful fallbacks, add timeouts and retries for script loading, log failures with the remote name and version, and allow rolling back via the manifest or disabling the remote with a feature flag. The rest of the application must remain usable.
Q8. How do you manage shared dependency version mismatches?
Use semantic version ranges in requiredVersion, singletons for libraries that must be unique, and strictVersion where incompatibility would be dangerous. Align major versions through a platform team and a shared dependency policy, and use automated dependency updates and CI checks that compare ranges across remotes. Coordinate major upgrades like React with a compatibility window.
Q9. Compare iframes, Web Components, and Module Federation.
Iframes offer strong isolation and security but poor UX, accessibility, and performance. Web Components provide a framework-agnostic contract with optional Shadow DOM style isolation, but have SSR and styling challenges and limited React 18 prop support. Module Federation provides seamless integration and dependency sharing for JavaScript apps but requires version discipline and offers no isolation by default.
Q10. How do you prevent CSS conflicts between micro frontends?
Scope styles with CSS Modules or CSS-in-JS, enforce class-name prefixes with linting, avoid global element selectors, use a shared design system with tokens, use cascade layers for predictable precedence, and use Shadow DOM where strong isolation is required.
Q11. How do you deploy micro frontends independently?
Each remote has its own pipeline that builds and publishes immutable, versioned assets to a CDN. A manifest or discovery service maps each remote name to its current version URL. Releasing updates the manifest, which also enables rollbacks and canary releases. The host loads remotes dynamically from the manifest, so it does not need to be rebuilt.
Q12. What are the downsides of micro frontends?
Operational complexity, more network requests and potential duplicated dependencies, runtime integration failures that a type checker cannot catch, inconsistent UX if governance is weak, harder end-to-end testing and debugging, and the need for a platform team to own the shell, tooling, and standards.
Q13. How would you share authentication across micro frontends?
The shell performs authentication (for example OIDC with PKCE) and exposes a small auth client with methods like getAccessToken() and onSessionExpired(). Tokens are held in memory or handled by a backend-for-frontend with HttpOnly cookies. Remotes never implement login, never persist tokens themselves, and receive tokens only through the shared client.
Q14. What is the role of the shell (host)?
It provides the layout, top-level routing, authentication, global error handling, shared services (logging, analytics, feature flags, i18n), the shared dependency baseline, and the mechanism for discovering and loading remotes. It should contain little business logic so that it changes rarely.
Q15. How would you share types between a host and remote?
Publish type declarations from the remote, either as an npm package generated in CI or using Module Federation 2.0’s automatic type generation, which produces and downloads @mf-types for remotes. Treat the exposed props as a versioned contract and add contract tests.
Q16. What is single-spa, and how does it relate to Module Federation?
single-spa is a framework for orchestrating multiple applications on one page: it registers apps with activity functions (usually route-based) and calls their bootstrap, mount, and unmount lifecycles. It does not prescribe how code is loaded. Module Federation solves loading and dependency sharing. They are often used together.
Q17. How do you test a federated application end to end?
Run each remote standalone for its own tests, use contract tests for exposed modules and events, and run Playwright tests against an integration environment that composes the versions about to be released. In each remote’s pipeline, test the new version against the current production versions of the host and other remotes.
Q18. Can micro frontends use different frameworks?
Technically yes, using Web Components or single-spa adapters, but it increases bundle size, cognitive load, and design inconsistency. Most enterprises standardize on one framework and version range and allow exceptions only for legacy migration, using the strangler fig pattern to replace old apps incrementally.
Q19. What is the strangler fig pattern in frontend migration?
Incrementally replacing a legacy application by routing specific paths or features to new micro frontends while the old application continues serving everything else. Over time, more routes move to the new system until the legacy app can be retired. A shell or reverse proxy decides which system handles each route.
Q20. How do you keep performance acceptable with many remotes?
Load remotes lazily by route, preload likely next remotes, share heavy dependencies, monitor per-remote bundle budgets, avoid duplicated libraries, use HTTP caching and CDNs with immutable versioned assets, and measure Core Web Vitals per route with remote versions as dimensions.
Q21. What should and should not be shared between micro frontends?
Share libraries that must be unique (React, router, design system) and stable cross-cutting services (auth, logging, i18n). Do not share domain state, business logic, or utility libraries that couple teams’ release cycles. Some duplication is an acceptable cost of independence.
Q22. What is the difference between Module Federation 1.0 and 2.0?
Version 1 is the original webpack 5 plugin with a runtime tied to webpack internals. Version 2 (the @module-federation/enhanced packages) decouples the runtime from the bundler, adds a manifest format, runtime plugins, dynamic remote registration APIs, type hinting, DevTools support, and works with Rspack, Rsbuild, Vite, and others.
Key Takeaways
- Micro frontends solve team-scaling problems and add operational complexity; use them deliberately.
- Module Federation loads remote modules at runtime and negotiates shared dependencies; React must be a singleton.
- Use an async bootstrap boundary, dynamic remotes from a manifest, and versioned immutable assets.
- Keep coupling loose: URLs, events, and host-provided props and services.
- Wrap every remote in error and Suspense boundaries, and test contracts as well as code.
← Part 3: Advanced React 18: Concurrency, Suspense, and Performance · All chapters · Part 5: HTML, CSS, SCSS, and Responsive Design →







