Adrian Spiridon

Micro Frontends Architecture Guide

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

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:

CapabilityBackendFrontend

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 postMessage protocol

  • a 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.

Diagram

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-federation

  • webpack 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 library

No 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 widgets

This is the classic runtime federation model.

2.6.3. Example 3: Application integrating another application

Application A
  -> opens / embeds Application B
      via iframe / popup / tab

This 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 remotes

This 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.

PrimitiveIntegration timeMain 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

Diagram

3.2. Explanation

The flow separates the main options:

PathMeaning

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.

Diagram

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 conceptProcess/IPC analogy

Shell page

Process A / host process

iframe, popup, window, or tab

Process B / external process

Browser boundary

Process/security boundary

postMessage

IPC message passing

Origin check

Trust boundary / process identity check

iframe sandbox attribute

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.

Diagram

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 targetOrigin

  • always validate event.origin

  • validate 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 BroadcastChannel is not used

  • logout 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 UI

Typical 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 webhook

In 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

SituationPrefer

Parent page and iframe

window.postMessage

Opener and popup/window

window.postMessage with nonce/state validation

Same-origin tabs/windows

BroadcastChannel

Simple same-origin fallback

localStorage + storage event

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 id

The 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 sandbox attribute where appropriate

  • understand opaque-origin behavior when sandboxing without allow-same-origin

  • use explicit postMessage contracts

  • validate event.origin

  • use strict targetOrigin when sending messages

  • apply 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 postMessage

  • avoid accepting unsolicited messages

  • be careful with window.opener

  • understand the effect of noopener

  • avoid 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.

Diagram

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.js

An 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

ConceptLayerWhy it is analogous

Linux .so shared libraries

OS / binary runtime

A program links to a shared library at runtime using SONAMEs and symbols.

Windows .dll files

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 / ServiceLoader

JVM runtime / application extension mechanism

A host application discovers provider implementations at runtime based on a known interface and metadata under META-INF/services.

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:

  1. federation mechanism compatibility

  2. framework/component contract compatibility

Federation mechanism compatibility means the shell and remote must use compatible runtime-loading mechanisms.

For example:

  • @angular-architects/native-federation host with compatible Native Federation remotes

  • webpack 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.

<< Previous Post

|

Next Post >>