<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Microfrontends with Angular: Architecture, implementation and Best practices

A microfrontend applies the microservice principle to the presentation layer: instead of a single monolithic Angular application, the frontend is divided into independent pieces (often aligned to separate product teams), each with their own build, deploy and pipeline release cycle, composed together with runtime or build-time into a single user experience. The benefit The main one is not technical but organisational: different teams can release autonomously without coordinate a shared monolithic deployment. The equally real trade-off is additional complexity of composition, shared dependencies, and integration testing — microfrontends are not the choice right for each project, and should be adopted when the cost of coordination between teams already exceeds today the cost of the technical complexity they introduce.

Integration Patterns: Comparison

PatternProsConsWhen To Prefer It
Client-side (shell + remotes)Maximum runtime flexibility, real independent deploymentsComplexity of sharing dependencies, risk of duplicationMultiple teams, frequent releases independent
Server-side compositionNative SEO, no extra JS for compositionRequires dedicated server infrastructure (SSI/ESI)High traffic public content, SEO critical
Edge/reverse proxyCentralized composition, granular cache per fragmentProxy becomes a single point of failureMature infra team, strong need for edge caching
Web ComponentsFramework-agnostic, native DOM encapsulationCommunication between components less natural than a native frameworkTeams with heterogeneous frontend stacks (not only Angular)
iFrameTotal isolation, zero CSS/JS conflictsPoor UX, awkward cross-frame communication, zero SEOOnly for non-third party integration trust me

For most Angular enterprise teams, the client-side pattern with Module Federation remains the default choice: it balances runtime flexibility and natural integration with Angular's routing and dependency injection, without the excessive isolation of an iframe.

Module Federation (Webpack 5): Shell and Remote

The shell (host) dynamically loads remote bundles at runtime, sharing dependencies common ones (Angular, RxJS) as singletons to avoid downloading them multiple times.

// webpack.config.js — SHELL (host)
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        checkout: 'checkout@https://cdn.example.com/checkout/remoteEntry.js',
      },
      shared: { '@angular/core': { singleton: true, strictVersion: true }, 'rxjs': { singleton: true } },
    }),
  ],
};
// webpack.config.js — REMOTE (checkout)
new ModuleFederationPlugin({
  name: 'checkout',
  filename: 'remoteEntry.js',
  exposes: { './Module': './src/app/checkout/checkout.module.ts' },
  shared: { '@angular/core': { singleton: true, strictVersion: true }, 'rxjs': { singleton: true } },
});
// Routing shell — lazy load dinamico del remote
export const routes: Routes = [
  {
    path: 'checkout',
    loadChildren: () => import('checkout/Module').then(m => m.CheckoutModule),
  },
];

Single-SPA: Registration and Lifecycle

// Registrazione di un microfrontend Angular in single-spa
registerApplication({
  name: 'checkout',
  app: () => import('./checkout.app'),
  activeWhen: ['/checkout'],
});

// Lifecycle esposto dal microfrontend
export const { bootstrap, mount, unmount } = singleSpaAngular({
  bootstrapFunction: singleSpaProps => bootstrapApplication(CheckoutRootComponent),
  template: '',
});

Angular Elements / Web Components

// Esporre un componente Angular come Web Component standard
const app = await bootstrapApplication(ProductCardComponent);
const element = createCustomElement(ProductCardComponent, { injector: app.injector });
customElements.define('product-card', element);

The consumer, although not based on Angular, uses the component like any native HTML tag: — this is what makes Web Components is the right option when the shell or other microfrontends are not Angular.

Lighter alternatives: import map with dynamic ES modules (no build tool dedicated to federation, only native import() pointed to versioned URLs) for teams that want to avoid the configuration complexity of Module Federation, and pattern Nx monorepo when the microfrontends share the same repository anyway but want maintain independent build pipelines via affected-graph.

Dependency Sharing and Design System

Angular and RxJS should always be shared as singleton: true in Module Federation: without this, each remote downloads its own copy of the framework, inflating the bundle and risking it change detection incompatibility between shell and remote loaded together.

// package.json della libreria di design system condivisa — peerDependencies, non dependencies
{
  "name": "@azienda/mfe-ui",
  "peerDependencies": { "@angular/core": "^18.0.0", "rxjs": "^7.0.0" }
}

Publish core components and design tokens as a separate versioned library (not duplicated in each microfrontend), documented in Storybook — the exact same pattern as a design system centralized, with the only difference that here the consumers are independent microfrontends instead of separate applications.

Routing, Status and Communication between Microfrontends

Global routing (which microfrontend to show) remains the responsibility of the shell; each remote manages its own local routing (the internal sub-routes) in a completely independent — the shell does not need to know the internal routing structure of a remote, just its input path.

// Event bus condiviso con RxJS — comunicazione shell/remote senza accoppiamento diretto
export class MfeEventBus {
  private readonly subject = new Subject<{ type: string; payload: unknown }>();
  emit(type: string, payload: unknown) { this.subject.next({ type, payload }); }
  on(type: string) { return this.subject.pipe(filter(e => e.type === type), map(e => e.payload)); }
}

For shared status, three options with different trade-offs: the URL (the simplest and robust, but limited to serializable state), a event bus as above (decoupled but without its own persistent state), or a shared store published as a library singleton (more powerful but introduces implicit coupling between microfrontends that must agree on a common state form). The contract between shell and remote must always be explicit and typed — props incoming and outgoing events, never direct and undocumented access to the internal state of a other remote.

CI/CD, Testing and Deploy

Each microfrontend has its own independent pipeline: build, unit test, Storybook, and publish del remoteEntry.js fingerprinted on CDN, without depending on the deployment of other microfrontends.

# GitHub Actions — sintesi pipeline per un singolo microfrontend
jobs:
  build-and-deploy:
    steps:
      - run: npm ci
      - run: npm run build -- --configuration production
      - run: npm test -- --watch=false
      - run: npx cypress run
      - run: aws s3 cp dist/checkout s3://cdn.example.com/checkout/$GITHUB_SHA --recursive
      - run: node scripts/update-manifest.js checkout $GITHUB_SHA
// scripts/update-manifest.js — aggiorna il manifest che la shell legge per risolvere i remoteEntry
const manifest = JSON.parse(fs.readFileSync('manifest.json'));
manifest[process.argv[2]] = `https://cdn.example.com/${process.argv[2]}/${process.argv[3]}/remoteEntry.js`;
fs.writeFileSync('manifest.json', JSON.stringify(manifest, null, 2));

For integration between shell and remote, two levels of tests: contract test which verify that the remote exposes the expected interface (props/events) without deploying the entire shell, e E2E against a staging environment with the real shell and real remotes, reserved for critical cross-microfrontend flows — mock remotes in shell unit tests, don't just rely to that to validate real integration.

Performance and Optimization

<!-- Prefetch del remoteEntry del prossimo microfrontend probabile, senza bloccare il rendering corrente -->
<link rel="prefetch" href="https://cdn.example.com/checkout/remoteEntry.js" as="script" />
  • Lazy loading of each remote only when the corresponding route is visited, never in the initial shell bundle.
  • Fingerprinting of the remoteEntry (hash in the path, not in the filename for compatibility with Module Federation) for long cache without accidental invalidations.
  • Bundle analyzer for each remote independently, not just on the aggregate shell, to detect duplications of dependencies that are not shared correctly.
  • TTI/LCP measured per single microfrontend, not only on the overall page: a slow remote degrades the perception of the entire shell even if the rest is fast.

Security and Operations

# CSP che permette script solo dai domini CDN dei microfrontend fidati
Content-Security-Policy: script-src 'self' https://cdn.example.com; connect-src 'self' https://cdn.example.com;

HTTPS required on every CDN serving a remoteEntry, and CORS explicitly configured for allow the shell to load cross-origin scripts from microfrontend domains. Always manage the failure to load a remote with an explicit fallback component, never a blank page: an unresponsive secondary microfrontend should not impede the use of the rest of the application.

// Fallback route nella shell se un remote non è raggiungibile
loadChildren: () => import('checkout/Module')
  .then(m => m.CheckoutModule)
  .catch(() => import('./fallback/checkout-unavailable.module').then(m => m.CheckoutUnavailableModule)),

Monitoring must be set for a single microfrontend, not just an aggregate: error rate, deployment remoteEntry loading frequency and latency for each team, so a problem is identified immediately in the responsible microfrontend instead of generically "in the shell".

Scalability and Rollback

Each deployment of a remote is versioned (path with hash/SHA of the commit on CDN); rollback consists of repoint the manifest to the previous version, without needing a new deployment or rebuild — the fastest possible operation in case of regression.

# Rollback: ripunta il manifest alla versione precedente del remote
node scripts/update-manifest.js checkout $PREVIOUS_SHA

For smooth rollouts, use feature flag at the shell level (which version of the manifest serve to what percentage of users) instead of an infrastructure-level canary more complex to orchestrate for individual UI fragments.

Case Study 1: Marketplace with 4 Product Teams

A marketplace with 4 independent teams (search, product, checkout, account) has adopted Module Federation with centralized shell. Initial setup effort: ~120 person-hours for shell, manifest, and shared CI/CD pipeline. After 5 months: deployment frequency went from 1/week (monolith) to 12/week aggregated across the 4 teams, average rollback time from 25 minutes (complete rebuild) to 40 seconds (manifest update), 0 cross-team merge conflicts on frontend.

Case Study 2: SaaS Platform Expanding Internationally

A SaaS platform with teams spread across 3 time zones has adopted Web Components to enable a team with React stack to integrate into the existing Angular shell without rewriting anything. Effort: ~80 person hours for the first remote Web Component plus shared token design library. Result: time to market of first cross-team feature reduced from 6 to 3 weeks, no forced dependencies from a single frontend stack for new teams onboarding.

Pre-Start Operational Checklist

  • Clear business domain boundary between microfrontends, team aligned, non-arbitrary.
  • Shared design system released before first remote, not after.
  • Shell-remote contract (props/events) explicitly documented and versioned.
  • Independent CI/CD pipeline already ready for at least the first remote before adding a second.

30/60/90 Day Plan

Days 1-30

  • Shell and first remote in production with Module Federation — KPI: build success rate 100%, independent deployment verified.

Days 31-60

  • Second and third remote onboard, per-microfrontend monitoring active — KPI: error rate tracked separately for each remote.

Days 61-90

  • Rollback tested end-to-end, feature flags for active gradual rollouts — KPI: rollback time measured under one minute.

FAQ

Does Module Federation always duplicate Angular if not configured well?

Yes, without singleton: true in the shared section each remote downloads its own copy of the framework, inflating the bundle and risking incompatibility.

How do you manage routing when a remote is not reachable?

With an explicit fallback in the lazy loading chain, never letting the import error propagate as a blank page.

Do you really need Nx to create microfrontends with Angular?

No, it is useful for orchestrating multiple projects in the same repository, but Module Federation also works with completely separate repositories.

How many microfrontends are "too many"?

There is no fixed number; the red flag is when the cost of coordinating between remote contracts exceeds the benefit of independent deployments.

How do you test integrations between shell and remote?

With contract tests on individual remotes plus E2E on a staging environment with real shells and remotes for critical flows.

Do microfrontends slow down performance compared to a monolith?

Can, if not handled with correct lazy loading and prefetch; well configured, the impact is marginal compared to organizational benefits.

How do you share design tokens between different microfrontends?

With a separately published library, versioned and consumed as a peerDependency by each remote.

Web Components or Module Federation?

Module Federation when all microfrontends are Angular; Web Components when the stack is heterogeneous or framework-agnostic isolation is needed.

How do I quickly rollback a single microfrontend?

Repointing the manifest to the previous version of the remoteEntry, without the need for a new deployment.

Do you need a dedicated shell or can it also be an application app?

A dedicated, minimal and stable shell is preferable, to reduce the risk that its redeploy impacts all the remotes at the same time.

How do you avoid CSS duplication between microfrontends?

With a shared design system as the sole source of basic styles, and encapsulation (Shadow DOM or rigorous naming convention) for the specific styles of each remote.

Do microfrontends make sense even for a single team?

Rarely: The main benefit is organizational, with a single team the runtime composition cost usually outweighs the benefits.

Common Mistakes to Avoid

  • Angular not shared as singleton: causes framework duplication and change detection bug between shell and remote.
  • No UX fallback for unreachable remotes: Turn an isolated issue into a blank page for the entire application.
  • Undocumented shell-remote contract: each team guesses the interface, causing silent regressions on each deployment.
  • Arbitrary domain boundaries between microfrontends: Generate excessive cross-remote communication instead of reducing coupling.
  • Duplicate design system instead of shared: Produces visual inconsistency between microfrontends within a few weeks.
  • No contract tests between shell and remote: integration regressions are only discovered in production.
  • Deploy unversioned remotes: makes rollback as slow and risky as a full rebuild.
  • Aggregate-only monitoring on the shell: Prevents you from quickly identifying which microfrontend is causing a problem.
  • iFrame used for internal composition between trusted teams: introduces zero communication and SEO complexity without the real need for total isolation.
  • No prefetching of probable remotes: each navigation to a new microfrontend pays the full cost of cold loading.

How to Check

  • Build each isolated remote: npx webpack --config webpack.config.js and check for errors.
  • E2E tests on real shells + remotes in staging: npx cypress run.
  • Manifest verification: check that each entry points to a remoteEntry.js actually reachable (curl -I on each URL).
  • Control bundle size per individual remote with a bundle analyzer, not just on the aggregate shell.
  • Fallback test: Temporarily disable a remote and verify that the shell displays the fallback component instead of an error.
  • Active monitoring: Verify that error rate, deployment frequency, and latency are tracked separately for each microfrontend.

Conclusion

Microfrontends with Angular solve an organizational problem, not primarily a technical one: the need for multiple teams to release autonomously without coordinating a shared monolith. Module Federation with shared dependencies such as singletons, a centralized design system, contracts explicit between shell and remote, and a versioned manifest-based rollback are the elements that distinguish a truly sustainable microfrontend architecture from added complexity without real benefit.

Do you want a skeleton repo to get started or an evaluation of your frontend architecture existing? Request a technical audit: in a few hours of analysis it is possible to identify the Correct domain boundaries and the key risks of a microfrontend adoption for your team.

💬 Reader notes

0 notes

Write a note

Share your opinion, a suggestion or a compliment

Latest notes

No notes yet. Be the first to comment!