Micro Frontends Architecture Guide
- 19 min read
Choosing a technology stack or integration pattern is rarely a neutral decision. In large enterprises, ideas like Micro Frontends can easily become the default answer through inertia: they sound modern, they promise reuse and autonomy, and teams are often expected to adopt them before the real costs are understood.
This guide is about slowing that decision down.
Micro Frontends can be the right architecture, but only when the need for runtime composition and independent frontend deployment is real. Otherwise, a simpler modular frontend design — using libraries, clear boundaries, lazy loading, and a well-structured monorepo — may provide most of the benefits with fewer operational headaches later.
TL;DR
Do not start with Micro Frontends.
Start with modular frontend design, libraries, lazy routes, and monorepo boundaries.
Use MFEs only when independent frontend deployment or strong isolation is a real requirement.
Browser-boundary composition is application-to-application integration through iframe, popup, tab, or window.
Runtime federation is dynamic frontend module loading.
Libraries are build-time reuse; federated modules are runtime reuse; applications are full deployable UI units.
Do not pay distributed-systems costs to solve a modularity problem.
- 1. MFEs: What, When, Pitfalls, and Hype
- 2. Relationship Between Libraries, Runtime Federation, and Applications
- 3. Frontend Architecture / MFE Decision Flow 2026 - Angular-Oriented
- 4. Browser-Boundary Composition
- 5. Runtime Federation
1. MFEs: What, When, Pitfalls, and Hype
In this guide, I use Micro Frontend to mean:
Independent frontend deployment + runtime composition.
Think of it as the frontend counterpart of the microservices idea.
In a microservices-based enterprise architecture, a department or platform team may expose a reusable business capability as a backend service. In a Micro Frontend architecture, the same department may also expose the corresponding frontend capability as a separately deployable UI.
For example:
| Capability | Backend | Frontend |
|---|---|---|
Customer search | Customer Search Service | Customer Search MFE |
Document viewer | Document Service | Document Viewer MFE |
Payment approval | Payment/Approval Service | Payment Approval MFE |
Reporting | Reporting Service | Reporting UI MFE |
The promise is that other applications can reuse a complete business capability without reimplementing both backend and frontend logic.
That does not mean Micro Frontends should be used merely because they sound modern.
They need a strong justification.
1.1. When MFEs Make Sense
Micro Frontends make the most sense when at least some of the following are true:
different domain teams need independent release cycles
the remote UI is owned, developed, tested, and deployed by another team
the remote UI represents a mostly self-contained business capability
the exposed integration contract is stable and well understood
the remote UI has a separate lifecycle from the shell application
the remote UI may need to be reused by several consuming applications
the remote UI comes from a legacy system or vendor system that cannot be deeply integrated
strong browser-level isolation is required
the organization accepts the operational cost of independently deployed frontend units
A good MFE should be mostly self-contained.
It should not have an "umbilical cord" to the shell or parent application for every business behavior change.
If every change in the MFE requires a coordinated change in the shell, then the MFE is probably not a real autonomous frontend capability. It is more likely a distributed library with extra operational complexity.
1.2. Stable Contract Requirement
The exposed contract of an MFE should be relatively stable.
Examples of stable contracts:
a route exposed by a remote application
a well-defined
postMessageprotocola versioned event/message schema
a standardized domain workflow
a stable widget/component API
a documented integration protocol
a domain contract that rarely changes
Ideally, the contract is standardized, well documented, and treated almost like an API specification.
If the contract changes frequently, consumers become tightly coupled to the producer. At that point, the MFE loses many of the benefits of independent deployment.
Important | A Micro Frontend is valuable only if the remote can evolve and deploy with reasonable independence. If every MFE deployment requires a shell deployment, then the architecture has the drawbacks of distribution without the benefits of autonomy. |
1.3. Pitfalls and Hype
MFEs have gone through a hype cycle similar to microservices.
Many teams adopted microservices to solve modularity problems, but ended up creating distributed-systems problems.
The same trap exists with Micro Frontends.
The usual failure modes include:
version compatibility issues
runtime integration failures
stale CDN/browser cache issues
difficult local development
duplicated dependencies
inconsistent UI/UX
unclear ownership boundaries
fragmented design systems
deployment choreography problems
remote availability problems
poor observability
unclear rollback strategy
cross-MFE state coupling
hidden communication protocols
runtime errors that would previously have been compile-time errors
The core issue is that MFEs deliberately move some integration concerns from compile time to runtime.
This gives more deployment autonomy, but it also creates more runtime failure modes.
1.4. Principle
My core architectural rule is:
Prefer static or compile-time composition by default. Use runtime composition only when runtime autonomy is the requirement.
In backend architecture:
Do not create microservices when package/module boundaries would solve the problem.
In frontend architecture:
Do not create MFEs when modular libraries and lazy routes would solve the problem.
2. Relationship Between Libraries, Runtime Federation, and Applications
Teams often mix up these options, as if they were mutually exclusive or all lived at the same level.
They do not.
They are different primitives that operate at different levels of granularity and at different integration times.
2.1. The Three Primitives
At a practical level, frontend architecture can be understood through three reusable primitives:
Library
the most granular reusable asset
usually consumed at build time
can depend on other libraries
Federated module / runtime federation
a runtime composition and exposure mechanism
can internally use libraries
allows an application to load functionality dynamically at runtime
Application / standalone UI
a full standalone deployable UI unit
can use libraries at compile time
can use runtime federation internally
can integrate another application through browser boundaries such as iframe, popup, or separate tab/window
2.2. Library
A library is the smallest reusable building block in this model.
Examples:
shared UI components
shared TypeScript types
shared utilities
Angular feature libraries
domain contract packages
design system packages
A library is normally consumed at build time.
Example:
{
"dependencies": {
"@company/shared-ui": "1.4.2",
"@company/customer-contracts": "2.1.0"
}
}A library may use other libraries internally, which is why the diagram shows:
Library → Library
That is normal composition at the smallest level.
What a library does not provide is independent runtime deployment.
A library is not an MFE by itself.
2.3. Federated Module / Runtime Federation
A federated module is a runtime-exposed unit of functionality.
Examples:
./routes./CustomerSearchComponent./Widget
In practical terms, runtime federation is usually implemented through:
@angular-architects/native-federationwebpack Module Federation
Rspack/Rsbuild or equivalent Module Federation ecosystem tools
A federated module can internally use many libraries.
That is why the diagram shows:
Federated Module → Library
A federated module sits between libraries and applications:
more dynamic than a library
more granular than a full standalone application
Its purpose is not merely code reuse, but runtime composition.
2.4. Application / Standalone UI
An application is the coarse-grained deployable UI unit.
Examples:
an Angular SPA
a React SPA
a Vue SPA
a server-rendered JSP application
a vendor portal
a legacy web application
An application can use libraries directly at compile time.
That is why the diagram shows:
Application → Library
An application can also use runtime federation internally.
That is why the diagram shows:
Application → Federated Module
This means an application can act as a shell and dynamically load federated functionality at runtime.
Finally, an application can integrate another application through browser boundaries such as:
iframe
popup
separate browser window
separate browser tab
That is why the diagram shows:
Application → Application
This is where browser-boundary composition fits.
2.5. Important Interpretation
A key takeaway here is:
Browser-boundary composition is best understood as application-to-application integration, while runtime federation is an internal runtime composition mechanism.
This distinction matters because people often mix up the following ideas:
a library
a federated module
a full standalone application
the act of integrating one application into another application
They are related, but not identical.
2.6. Examples
2.6.1. Example 1: Simple application using libraries
Application
-> shared UI library
-> customer contracts library
-> utility libraryNo MFE is involved here.
This is ordinary compile-time composition.
2.6.2. Example 2: Application using runtime federation
Application
-> shared libraries
-> runtime federation
-> remote routes
-> remote widgetsThis is the classic runtime federation model.
2.6.3. Example 3: Application integrating another application
Application A
-> opens / embeds Application B
via iframe / popup / tabThis is browser-boundary composition.
2.6.4. Example 4: Browser-boundary remote that also uses runtime federation internally
Application A
-> opens / embeds Application B
Application B
-> shared libraries
-> runtime federation
-> internal remotesThis is why the distinction matters:
browser-boundary composition describes the external integration style
runtime federation describes an internal runtime composition style
The same application can participate in both.
2.7. Key Takeaway
Libraries, runtime federation, and applications are different primitives. They can be combined, but they solve different problems and operate at different levels.
| Primitive | Integration time | Main purpose |
|---|---|---|
Shared library | Build time | Code reuse with compile-time safety |
Federated module / runtime federation | Runtime | Runtime exposure and dynamic loading of functionality |
Application / standalone UI | Deployment/runtime | Full UI boundary and deployable unit |
3. Frontend Architecture / MFE Decision Flow 2026 - Angular-Oriented
This decision flow is a quick map of the main architectural paths.
The flow is intentionally Angular-oriented, because the runtime federation branch assumes an Angular shell and Angular-compatible remotes.
3.1. Diagram
3.2. Explanation
The flow separates the main options:
| Path | Meaning |
|---|---|
Modular monolith / monorepo | No independent frontend deployment is required. Use modular libraries, lazy routes, strong compile-time boundaries, and smart CI/cache. |
Browser-boundary composition | The remote UI runs as a separate browser context, such as an iframe or separate popup/window/tab. |
Runtime federation | The shell dynamically loads compatible remote frontend modules at runtime. |
4. Browser-Boundary Composition
4.1. Overview
Browser-boundary composition is the pattern where independently deployed UIs are integrated through separate browser contexts.
Examples:
iframe embedded inside the shell page
popup opened by the shell
separate browser window
separate browser tab
The key point is that the remote UI remains a separately hosted web application. The shell does not import the remote’s internal JavaScript modules. Instead, the shell and remote communicate through an explicit communication mechanism.
This pattern is especially relevant when the remote UI cannot be integrated more deeply.
For example, a legacy SSR JSP application is already a complete web application. It may not expose Angular routes, standalone components, or federated modules. In such a case, integration through iframe or separate tab/window may be the only realistic option.
4.2. Analogy: Process Isolation + IPC
Think of browser-boundary composition like this:
It is like running a separate process and communicating with it through an explicit IPC protocol.
Mapped back to browser terms:
| Browser concept | Process/IPC analogy |
|---|---|
Shell page | Process A / host process |
iframe, popup, window, or tab | Process B / external process |
Browser boundary | Process/security boundary |
| IPC message passing |
Origin check | Trust boundary / process identity check |
iframe | Restricted process/container permissions |
CSP | Execution/security policy |
Nonce/state parameter | Correlation ID / handshake token |
Query parameters | Startup arguments |
For example, imagine a messenger client communicating with a server.
The messenger client does not directly execute the server’s internal code. It sends messages using an agreed protocol. The server processes the message and sends back a response.
In browser-boundary composition:
the shell does not execute the remote UI’s internal modules
the remote UI runs separately
communication happens through explicit messages or backend-mediated signals
both sides must agree on the message contract
That is very different from runtime federation, where the shell loads and executes remote frontend modules inside the same JavaScript runtime.
4.3. Communication
Browser-boundary composition uses explicit communication mechanisms between separate browser contexts.
The two UIs do not share an Angular component tree, dependency injection context, router, or direct module graph. They communicate through browser APIs or backend-mediated events.
4.3.1. Option 1: Direct window.postMessage
window.postMessage is the default mechanism for direct communication between browser contexts when one side has a reference to the other.
Typical cases:
parent page and iframe
opener window and popup
popup and opener window
Example parent to iframe:
iframe.contentWindow.postMessage(
{
type: 'SET_CONTEXT',
correlationId: 'abc-123',
customerId: 'C001'
},
'https://remote.company.com'
);Example iframe or popup back to shell:
window.parent?.postMessage(
{
type: 'CUSTOMER_SELECTED',
correlationId: 'abc-123',
customerId: 'C001'
},
'https://shell.company.com'
);For popups, the remote usually uses:
window.opener?.postMessage(
{
type: 'CUSTOMER_SELECTED',
state: 'one-time-state-value',
customerId: 'C001'
},
'https://shell.company.com'
);Security rules:
always use a specific
targetOriginalways validate
event.originvalidate message shape
use nonce/state/correlation IDs when needed
do not accept arbitrary unsolicited messages
4.3.2. Option 2: BroadcastChannel
BroadcastChannel is a browser-native pub/sub mechanism for same-origin browser contexts.
It is useful when several same-origin tabs, windows, or iframes need to listen to the same local-browser event.
Example:
const channel = new BroadcastChannel('customer-events');
channel.postMessage({
type: 'CUSTOMER_SELECTED',
customerId: 'C001'
});
channel.onmessage = event => {
if (event.data?.type === 'CUSTOMER_SELECTED') {
handleCustomerSelected(event.data.customerId);
}
};Typical uses:
same-origin tab-to-tab communication
logout synchronization
theme/language changes
local browser coordination
Limitations:
not a cross-origin integration mechanism
broadcasts to all listeners on the channel
not ideal for untrusted remotes
message shape must still be validated
4.3.3. Option 3: Browser Storage
Browser storage is mainly a fallback communication mechanism, not a message broker.
The usual model is localStorage plus the storage event. One same-origin browsing context writes a value to localStorage, and other same-origin browsing contexts receive a storage event.
The document that performs the write should not rely on receiving its own storage event.
sessionStorage is more limited because it is scoped to a top-level browsing context. Do not treat it as a generic tab-to-tab communication mechanism.
Example:
localStorage.setItem(
'customer-event',
JSON.stringify({
type: 'CUSTOMER_SELECTED',
customerId: 'C001',
at: Date.now()
})
);Listener:
window.addEventListener('storage', event => {
if (event.key !== 'customer-event') {
return;
}
const message = JSON.parse(event.newValue);
if (message.type === 'CUSTOMER_SELECTED') {
handleCustomerSelected(message.customerId);
}
});Typical uses:
simple same-origin tab coordination
fallback when
BroadcastChannelis not usedlogout synchronization
Limitations:
same-origin only
mainly useful as a fallback
clunky for complex protocols
not suitable for high-frequency communication
storage is not a proper message broker
4.3.4. Option 4: Backend-Mediated Communication
Sometimes the communication should not be browser-local at all.
If the event belongs to the system or backend domain, both UIs can communicate through a backend or broker.
Examples:
WebSocket/WSS with raw JSON messages
SSE for server-to-browser push
REST APIs plus polling
backend message broker behind a WebSocket gateway
Example topology:
Parent/Shell UI
<-> Backend / WebSocket gateway / broker
<-> Remote UITypical uses:
multi-user updates
multi-device synchronization
backend job progress
notifications
workflow status updates
domain events
For many applications, raw JSON messages over WebSocket/WSS are enough. STOMP is optional and only useful if the project wants broker-style topic/queue semantics.
4.3.5. Option 5: Navigation / Redirect Callback Flow
Some browser-boundary integrations do not need an ongoing message channel between the originating UI and the remote UI.
A common example is an external payment, signing, login, or approval workflow.
In this model, the originating application redirects the user to an external application, or opens it in a separate tab/window. The originating application passes a return URL and a correlation identifier. When the external workflow finishes, the external application redirects the user back to the originating application.
Example flow:
E-commerce UI
-> opens Payment Provider UI
with paymentForId / orderId / state / returnUrl
Payment Provider UI
-> user completes payment
Payment Provider UI
-> redirects back to E-commerce UI
with state / paymentId / paymentSessionId / orderId
E-commerce Backend
-> verifies payment status server-side
by calling Payment Provider API
and/or by processing a webhookIn this pattern, the redirect back to the originating application is primarily a user-navigation and correlation mechanism.
It should not be treated as final proof that the external action succeeded.
For payment flows, the merchant application should normally validate the returned payment/session/order identifier through a trusted backend channel, or wait for a signed webhook/event from the payment provider.
The browser callback tells the application that the user returned. The backend verification tells the application whether the payment is actually valid and settled enough for the next business step.
Treat returned IDs as correlation handles, not as proof.
4.3.6. Communication Rule of Thumb
| Situation | Prefer |
|---|---|
Parent page and iframe |
|
Opener and popup/window |
|
Same-origin tabs/windows |
|
Simple same-origin fallback |
|
Backend/system-level events | WebSocket/WSS, SSE, API, or backend-mediated broker |
External payment/signing/login/approval workflow | Navigation / redirect callback flow with backend verification |
Multi-user or multi-device synchronization | Backend-mediated communication |
4.4. Iframe Variant
In the iframe variant, the shell embeds the remote UI inside its own page.
Example:
<iframe
src="https://remote.company.com/customer-search"
sandbox="allow-scripts allow-forms">
</iframe>Typical uses:
legacy application embedding
vendor widgets
secure payment frames
document viewers
reporting screens
BI dashboards
isolated admin tools
Communication normally uses one of the mechanisms above, most often direct window.postMessage.
If the iframe is sandboxed without allow-same-origin, the embedded page is treated as having an opaque origin. This affects cookies, storage, BroadcastChannel, and some origin checks. That can be desirable for isolation, but it must be designed intentionally.
4.5. Separate Popup / Window / Tab Variant
In the separate window/tab variant, the shell opens the remote UI as a separate workflow.
Example:
const state = crypto.randomUUID();
const childWindow = window.open(
`https://remote.company.com/customer-search?state=${state}`,
'_blank',
'popup,width=1200,height=800'
);The remote usually returns the result through one of the communication mechanisms above.
In a direct postMessage flow, the shell must validate both origin and the one-time state value.
In a navigation / redirect callback flow, the remote redirects the browser back to a return URL owned by the originating application. This is common for payment authorization, external signing, login, and approval workflows.
Example:
Shell / E-commerce UI
-> opens external workflow with state + returnUrl
External workflow
-> redirects back to returnUrl with state + external workflow idThe returned external identifier should be treated as a correlation handle. The originating backend should verify the final state through a trusted backend call or webhook before accepting the business outcome.
Typical uses:
legacy workflows
large external tools
document signing
payment authorization
external login or consent flows
external approval workflows
customer/product pickers
vendor portals
report viewers/editors
4.6. Security Notes
Browser-boundary composition can provide strong isolation, but only if browser security mechanisms are used correctly.
For iframe-based integration:
use the
sandboxattribute where appropriateunderstand opaque-origin behavior when sandboxing without
allow-same-originuse explicit
postMessagecontractsvalidate
event.originuse strict
targetOriginwhen sending messagesapply CSP
avoid over-permissive iframe capabilities
avoid leaking sensitive data in URLs
For popup/window/tab integration:
use nonce/state correlation
validate message origin when using
postMessageavoid accepting unsolicited messages
be careful with
window.openerunderstand the effect of
noopeneravoid long-lived tokens in query parameters
prefer short-lived, one-time-use state values
for redirect callback flows, treat returned IDs as correlation handles, not as proof
verify payment/signing/approval results through a trusted backend call or signed webhook where applicable
Important | Browser-boundary composition does not automatically mean secure composition. The security depends on origin design, sandboxing, CSP, message validation, token handling, and lifecycle management. |
5. Runtime Federation
5.1. Overview
Runtime federation is the pattern where the shell dynamically loads compatible frontend modules from independently deployed remotes.
Unlike browser-boundary composition, the remote does not remain a separate browser page or separate browser context. Its exposed code is loaded into the shell’s JavaScript execution environment.
The browser usually downloads the remote from a web-accessible URL, such as:
https://orders.company.com/remoteEntry.js
https://orders.company.com/remoteEntry.json
https://cdn.company.com/orders/remoteEntry.jsAn npm registry or Artifactory repository may be part of the build and deployment pipeline, but the browser typically does not load the remote directly from a package registry at runtime.
5.2. Analogy: Dynamic Linking
Runtime federation loads frontend modules at runtime, similar to how other platforms load external code or modules dynamically.
In this model:
the shell is like the host process
the remote is like a dynamically loaded library or plugin
the exposed route/component is like an exported function/class
the
remoteEntry/ manifest is like runtime metadata used to find and load the module
Short version:
Runtime federation is frontend dynamic linking.
5.3. Comparable Concepts
| Concept | Layer | Why it is analogous |
|---|---|---|
Linux | OS / binary runtime | A program links to a shared library at runtime using SONAMEs and symbols. |
Windows | OS / binary runtime | An executable loads an external binary library dynamically. |
Java ClassLoaders | JVM runtime | An application can load classes or JARs dynamically at runtime. |
Java Service Provider Interface / | JVM runtime / application extension mechanism | A host application discovers provider implementations at runtime based on a known interface and metadata under |
Java applets | Browser/JVM runtime, historically | The browser loaded external Java code into a runtime environment. This is historically relevant as an example of remote runtime-loaded UI code, although applets are obsolete and should not be used as a modern architectural model. |
5.4. Compatibility Warning
A federated remote must be compatible at two levels:
federation mechanism compatibility
framework/component contract compatibility
Federation mechanism compatibility means the shell and remote must use compatible runtime-loading mechanisms.
For example:
@angular-architects/native-federationhost with compatible Native Federation remoteswebpack Module Federation host with compatible webpack/Rspack/Rsbuild Module Federation remotes
Framework/component contract compatibility means the shell must be able to actually consume what the remote exposes.
For example, an Angular shell can directly consume:
Angular routes
Angular standalone components
Angular modules, in older setups
But an Angular shell cannot directly consume an arbitrary React component just because both sides use webpack Module Federation.
A React remote may be technically loadable as JavaScript, but it is not directly renderable as an Angular component. It needs an adapter, such as:
a mount/unmount function
a Web Component wrapper
an iframe
a single-spa-style lifecycle adapter
Important | Most teams underestimate the compatibility problem. Runtime federation does not remove versioning concerns. It moves many of them from build time to runtime. The shell and remote must remain compatible across federation runtime, framework version, shared dependencies, exposed module names, route/component contracts, deployment behavior, and cache behavior. |
5.5. Communication
In runtime federation, communication is much closer to normal in-process application communication.
The remote code is loaded into the shell’s JavaScript execution environment. From that point of view, calling exposed functionality is similar to calling a normal imported module, function, route, or component.
Put differently:
Runtime federation communication is not fundamentally browser-to-browser messaging. It is mostly regular JavaScript/Angular interaction after the remote has been loaded.
For example, an Angular shell can load remote routes:
{
path: 'customers',
loadChildren: () =>
loadRemoteModule('customerRemote', './routes')
.then(m => m.CUSTOMER_ROUTES)
}Or load a remote component:
{
path: 'customer-widget',
loadComponent: () =>
loadRemoteModule('customerRemote', './CustomerWidget')
.then(m => m.CustomerWidgetComponent)
}From there, the integration resembles normal Angular usage:
inputs/outputs for components
services and dependency injection, if intentionally shared
Angular router contracts
function calls
RxJS streams
signals/state APIs
shared contract libraries
A shared service or singleton can make communication feel easy, but it can also reintroduce tight coupling between shell and remote. Prefer explicit contracts for cross-team integration.
This is the main difference from browser-boundary composition.
Browser-boundary composition communicates through explicit browser or backend messaging, such as postMessage, BroadcastChannel, storage events, or backend-mediated messages.
Runtime federation communicates primarily through the same mechanisms a normal application uses internally, because the remote is loaded as executable code inside the shell.
Important | Runtime federation feels like normal function/module usage, but it is still runtime integration. The remote must remain compatible with the shell across federation mechanism, Angular/framework version, shared dependencies, exposed module names, and route/component contracts. |