Microfrontends with Angular and Nx: what nobody tells youCode & Development

Code & Development · 2 Aug 2026

Microfrontends with Angular and Nx: what nobody tells you

Every tutorial teaches how to set up microfrontends in minutes, but few reveal the architectural chaos that emerges after months of production deploys.

Genildo Souza2 Aug 2026Read: 15 min

Real-world challenges of Microfrontends with Angular

  • Microfrontends often take three months to implement and nine months to stabilize due to unforeseen production issues.

  • Sharing global state between remotes negates the independence of deploys, making the system coupled and difficult to maintain.

  • CSS conflicts between remotes are non-deterministic issues that require the use of strict namespaces to prevent style leakage.

  • Using Nx with strict versioning configurations is essential to avoid silent failures caused by library incompatibilities.

  • Maintaining different versions of Angular in the same Nx monorepo is unfeasible, requiring global migrations across the entire workspace.

Every tutorial shows how to set up Module Federation in 20 minutes. None show what happens 6 months later in production — the CSS that leaks between remotes, the Angular version that crashes the shell, the shared state that made independent deployment impossible, and the organizational cost that the architectural diagram fails to capture.


The reality that the tutorial doesn't show

The tutorial ends where the problem begins.

Most teams that deploy microfrontends reach production in 3 months and spend the next 9 months solving problems that no tutorial mentioned.

The model is seductive: independent teams, independent deploys, technology autonomy. In practice, the promised independence has a cost that only appears when one team tries to upgrade an Angular version while another is still in the previous cycle. Or when the CSS from the checkout remote starts affecting the shell's layout. Or when you discover that the shared state you put in a global service made independent deployment technically impossible.

A arquitetura de microfrontends não falha no código. Falha nas fronteiras que você não definiu.

— Microfrontends com Angular e Nx: o que ninguém conta

→ The number that matters In a survey of 20+ microfrontend projects from 2021-2023, the pattern was consistent: 3 months to implement, 9 months to stabilize. The problems of shared state, CSS leaking, and version mismatch don't appear in demos — they appear when teams start working in a genuinely independent way.

Microfrontend architecture doesn't fail in the code. It fails at the boundaries you didn't define.


The 5 production problems

What happens 6 months after deployment.

Problem 01 — Version mismatch — the silent crash: Team A uses Angular 21. Team B upgrades to Angular 22 with the new Signal Forms API. The shell loads both. Module Federation tries to reconcile the versions with singleton: true. Sometimes it works. Sometimes remote B crashes without an obvious error message.

Problem 02 — CSS leaking — the style that has no boundaries: The checkout remote defines .btn { background: red } without scope. The shell has .btn { background: blue }. Depending on the loading order, one overwrites the other. In production, the order changes with caching. The bug is non-deterministic.

Problem 03 — Global state — the deploy that isn't independent: To share the authenticated user, someone put the state in an NgRx Store shared among all remotes. Now, any change in the state shape requires a coordinated deploy of all remotes. The "independent" deploy is no longer independent.

Problem 04 — DX in development — 4 terminals for 1 feature: To run the checkout feature locally, the dev needs to start shell, catalog-mfe, admin-mfe, and checkout-mfe. The local setup time has tripled. The team's autonomy came with a DX overhead that no one accounted for.

Problem 05 — Bundle size — the sharing that didn't share: Each remote bundled its own copy of Angular Material because the configuration of shared did not include the secondary entry points (@angular/material/button, @angular/material/dialog). The total bundle became 3x larger than the original monolith.


Solution 01 — Version mismatch

Version governance: Nx as the referee

Module Federation allows different versions. Nx, by default, does not — and this restriction is a feature, not a bug.

typescript
// ❌ Configuração que causa crashes silenciosos
// module-federation.config.ts (remote checkout)
export const config: ModuleFederationConfig = {
  name: 'checkout',
  exposes: {
    './Module': './src/app/remote-entry/entry.module.ts',
  },
  shared: {
    // ❌ singleton sem requiredVersion = aceita qualquer versão
    // Se shell tem Angular 21 e remote tem Angular 22,
    // o Module Federation carrega a versão do shell silenciosamente
    '@angular/core': { singleton: true },
    '@angular/common': { singleton: true },
  },
};
typescript
// ✅ Governança de versão explícita com Nx
// libs/shared-mf-config/base.config.ts
import { shareAll } from '@nx/angular/module-federation';

export const baseConfig = {
  shared: {
    // ✅ requiredVersion: 'auto' lê do package.json do workspace
    // strictVersion: true → falha se versão incompatível
    '@angular/core': {
      singleton: true,
      strictVersion: true,
      requiredVersion: 'auto', // ← Nx resolve do root package.json
    },
    '@angular/router': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
    '@angular/forms': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
  }
};
bash
# Ver quais apps precisam ser (re)deployados após mudança em libs/auth
nx affected --target=build --base=main

# Output:
# Affected projects:
#   shell          ← host sempre afetado quando auth muda
#   checkout-mfe   ← usa AuthGuard da libs/auth
#   admin-mfe      ← usa AuthService da libs/auth
# NOT affected:
#   catalog-mfe    ← não depende de libs/auth

# Quando você ATUALIZA o Angular (major version):
nx migrate @angular/core@22
nx migrate --run-migrations
# Aplica migrações em TODOS os apps do workspace de uma vez

⚠️ The Angular rule in an Nx monorepo When you upgrade Angular in an Nx workspace, all apps in the workspace upgrade together. This is intentional — the alternative is a mixed-version hell. If two teams need different versions of Angular, they need separate repositories — and then they lose the advantages of the Nx monorepo.


Solution 02 — CSS leaking

Style isolation: ViewEncapsulation is not enough

Angular's ViewEncapsulation isolates component styles but does not resolve the global styles that each remote defines.

css
/* ❌ Sem namespace — vaza para o shell e outros remotes */
.card { background: #1a1a2e; border-radius: 8px; }
.btn  { background: #e94560; color: white; }
.overlay { z-index: 1000; }  /* ← conflita com overlays do shell */
css
/* ✅ Solução 1: Namespace SCSS por remote */
/* checkout-mfe/src/styles.scss */
.checkout-mfe {
  .card   { background: #1a1a2e; border-radius: 8px; }
  .btn    { background: #e94560; color: white; }
  .overlay { z-index: 100; }
}

/* No componente raiz do remote: */
/* <div class="checkout-mfe"><router-outlet /></div> */

/* ✅ Solução 2: CSS Custom Properties do Design System no shell */
/* O shell define as variáveis. Os remotes consomem. Nunca definem. */
:root {
  --ds-color-primary: #dd0031;
  --ds-color-surface: #161b22;
  --ds-radius-card: 8px;
  --ds-z-overlay: 200;
  --ds-z-modal:   300;
  --ds-z-toast:   400;
}

/* Remotes APENAS consomem variáveis — nunca definem novos z-index absolutos */

✅ The golden rule of CSS in MFEs The shell is the sole owner of global styles. Remotes consume CSS Custom Properties defined by the shell. No remote defines absolute z-index values — this is the hardest bug to debug in production, because z-index depends on the stacking context and the shell does not control the remote's context.


Solution 03 — Shared state

The state that destroys independent deployment

⚠️ The most common anti-pattern in MFEs Putting the authenticated user state in an NgRx Store shared via Module Federation among all remotes. It seems right — "it's global state, it should be in the global NgRx". The problem is that any change in the shape of this state requires that all remotes be updated and deployed at the same time. The "independent" deploy is no longer independent.

typescript
// libs/core/src/lib/event-bus.service.ts
// ✅ O Event Bus é o contrato entre remotes — não o shape do estado
import { Injectable } from '@angular/core';
import { Subject, filter, map } from 'rxjs';

export interface MfeEvent {
  type: string;
  payload: unknown;
  source: string;
}

export type UserAuthenticatedEvent = MfeEvent & {
  type: 'user.authenticated';
  payload: {
    userId:   string;
    tenantId: string;
    // ✅ Apenas os campos que outros remotes REALMENTE precisam
  };
};

export type CartUpdatedEvent = MfeEvent & {
  type: 'cart.updated';
  payload: { itemCount: number; total: number };
};

@Injectable({ providedIn: 'root' })
export class EventBusService {
  private bus$ = new Subject<MfeEvent>();

  emit(event: MfeEvent): void {
    this.bus$.next(event);
  }

  on<T extends MfeEvent>(type: T['type']) {
    return this.bus$.pipe(
      filter(e => e.type === type),
      map(e => e as T)
    );
  }
}

// ✅ No shell — emite após autenticação
// eventBus.emit({ type: 'user.authenticated', payload: { userId, tenantId }, source: 'shell' });

// ✅ No checkout-mfe — escuta sem saber nada do shell
// eventBus.on<UserAuthenticatedEvent>('user.authenticated').subscribe(...)

// Quando o shape do usuário muda, você versiona o evento:
// 'user.authenticated.v2' — backward compatible, remotes migram no seu ritmo
typescript
// ✅ Se você PRECISA de estado compartilhado, compartilhe o mínimo
export const AUTH_CONTEXT = new InjectionToken<AuthContext>('AUTH_CONTEXT');

export interface AuthContext {
  // Apenas o que é genuinamente necessário para TODOS os remotes
  userId:   string | null;
  tenantId: string | null;
  isAuthenticated: boolean;
  // ❌ NÃO coloque: permissions, roles, profile, preferences
}

Solution 04 — Developer Experience

Local development without 4 open terminals

json
// shell/src/assets/module-federation.manifest.json
// ✅ Dynamic Module Federation — a URL do remote é resolvida em runtime
{
  "checkout": "http://localhost:4201",
  "catalog":  "http://localhost:4202",
  "admin":    "http://localhost:4203"
}

// shell/src/assets/module-federation.manifest.staging.json
{
  "checkout": "https://checkout.staging.example.com",
  "catalog":  "https://catalog.staging.example.com",
  "admin":    "https://admin.staging.example.com"
}
bash
# ✅ Inicia o shell + checkout em modo live
# catalog e admin são servidos como static (sem live reload)
nx serve shell --devRemotes=checkout

# Inicia o shell + dois remotes em modo live
nx serve shell --devRemotes=checkout,catalog

# Com Dynamic Module Federation — aponta para staging:
# O dev desenvolve checkout localmente, catalog e admin vêm do staging
cp src/assets/module-federation.manifest.staging.json \
   src/assets/module-federation.manifest.json

→ The recommended Nx pattern for DX Configure a target serve-with-staging-remotes in the project.json of the shell. The dev starts a single terminal, the shell loads the local remote they are developing and the other remotes from staging. No mocks, no stubs — a real environment with a live remote.


Solution 05 — Bundle size

The sharing that didn't share anything

typescript
// ❌ Shared incompleto — cada remote tem sua própria cópia do Material
shared: {
  '@angular/material': { singleton: true, strictVersion: true },
  // Problema: @angular/material/button é uma entrada separada
  // → Cada remote vai empacotar sua própria cópia desses módulos
  // → Bundle total: shell(mat/button) + checkout(mat/button) = 3x
}
typescript
// ✅ shareAll com entradas secundárias — a configuração correta
import { shareAll } from '@nx/angular/module-federation';

// shareAll compartilha AUTOMATICAMENTE todos os pacotes do package.json
// Incluindo entradas secundárias como @angular/material/button
export const sharedDeps = shareAll({
  singleton: true,
  strictVersion: true,
  requiredVersion: 'auto',
});

// module-federation.config.ts de cada remote:
export const config: ModuleFederationConfig = {
  name: 'checkout',
  exposes: { './Module': './src/app/remote-entry/entry.module.ts' },
  shared: {
    ...sharedDeps,
  },
};

// Resultado com shareAll:
// Bundle antes: shell(300KB) + checkout(280KB) + catalog(275KB) = 855KB
// Bundle depois: shell(300KB) + checkout(80KB) + catalog(75KB) = 455KB
// Redução de ~47% no total transferido

✅ Integrated Nx bundle analysis Run nx build checkout --analyze to open the Webpack Bundle Analyzer and identify which packages are being duplicated between the shell and the remotes. Perform this analysis before the first production deploy — it is much harder to fix after the CDN cache is propagated.


When microfrontends are not the answer

Criteria

Microfrontends

Modular monolith with Nx

Team size

✅ 5+ parallel teams

✅ 1-3 teams — MFE cost is not worth it

Deployment cadence

✅ Teams with different cadences

✅ Same cadence — unified deploy is simpler

Business domains

✅ Genuinely independent domains

❌ High interdependence — MFE creates artificial boundaries

Initial bundle

❌ Larger — Module Federation overhead

✅ Smaller

Config complexity

❌ High — versions, CSS, state, DX

✅ Low

Local DX

⚠️ Degraded without careful configuration

✅ Simple — ng serve

→ The Nx recommendation The official Nx documentation is direct: "If you want to optimize builds and don't need independent deploys, use our Faster Builds with Module Federation guide." You can have incremental builds, smart caching, and a single repository without the complexity of independently deployed remotes. This is the right answer for most teams of up to 30 people.

The checklist before adopting microfrontends

  • 5+ teams that need to deploy with genuinely different cadences

  • Clear business domains that don't share state beyond authentication

  • Independent SLAs — a failure in checkout should not affect the catalog

  • Established design system via CSS Custom Properties before starting

  • Version governance — clear policy for Angular upgrades

  • Single team that "will grow" — anticipate with lazy modules, not MFE

  • Technical complexity as a goal — MFE is for organizational problems

  • ⚠️ Extensive shared state between remotes — redesign the boundaries first

  • ⚠️ Critical SEO — MFEs with Module Federation are CSR by default


Correct setup from scratch

Each step below mitigates a real production risk. Skipping one doesn't generate an immediate error — it generates the problem you will debug in production 6 months later.

Step 1 — Create the Nx workspace with Angular preset:

bash
npx create-nx-workspace@latest minha-org --preset=angular-monorepo

Mitigated risk: without the correct preset, the workspace doesn't configure @nx/enforce-module-boundaries automatically. Without this lint rule, remotes will import from each other directly — creating coupling that makes independent deployment impossible. You will only discover the problem when you try to deploy checkout without the catalog and the build breaks.


Step 2 — Generate the shell with Dynamic Module Federation:

bash
nx g @nx/angular:host shell --remotes=checkout,catalog,admin --dynamic=true

Mitigated risk: without --dynamic=true, the URL of each remote is hardcoded in the shell bundle at build time. Changing a remote's URL in production requires a rebuild and redeploy of the entire shell — destroying deployment independence. With the dynamic JSON manifest, you change the URL at runtime without touching the shell.


Step 3 — Create shared libs before any feature:

bash
nx g @nx/angular:lib libs/ui
nx g @nx/angular:lib libs/auth
nx g @nx/angular:lib libs/core

Mitigated risk: creating libs after remotes already exist means each remote already has its own version of components and services. Unifying later is a painful refactoring that teams resist for months. Creating them first forces the discipline of "shared code goes into libs" from the first commit — the coupling never exists to be removed.


Step 4 — Configure the base shared config in libs/shared-mf-config:

typescript
// shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' })

Mitigated risk: without a centralized base config, each remote defines its own shared — and invariably someone forgets to include the secondary entry points of Angular Material or CDK. The result is Problem 05: 3x larger bundle. With the base config in the monorepo, a single PR fixes all remotes at the same time.


Step 5 — Establish the Design System in the shell before the first remote: Define all CSS Custom Properties in shell/src/styles.scss and document the namespace convention (e.g., .checkout-mfe { ... }).

Mitigated risk: without centralized CSS tokens, each remote defines its own color, spacing, and z-index values. In 3 months you have 4 divergent design systems and non-deterministic CSS leaking (Problem 02). Refactoring this later means convincing 4 teams to stop delivering features to align CSS variables — a conversation that never happens.


Step 6 — Implement EventBusService before the first cross-remote flow: libs/core/src/lib/event-bus.service.ts with event types in libs/core/src/lib/events.ts.

Mitigated risk: without an established communication contract, the first dev who needs to pass data between remotes will put state in global NgRx — creating Problem 03. The EventBus forces the team to think about versioned contracts before creating state dependency. It is much easier to establish the right pattern before the first flow than to remove global state after 3 teams depend on it.


bash
# Estrutura de workspace que aguenta 2 anos de produção
minha-org/
├── apps/
   ├── shell/              # Host — roteamento e layout global
   ├── checkout-mfe/       # Remote — domínio de pagamento
   ├── catalog-mfe/        # Remote — domínio de catálogo
   └── admin-mfe/          # Remote — domínio administrativo
├── libs/
   ├── ui/                 # Design System — components, tokens CSS
   ├── auth/               # AuthGuard, AuthContext, EventBus types
   ├── core/               # EventBus, interceptors, error handler
   ├── shared-mf-config/   # Base config de Module Federation
   └── data-access/        # APIs compartilhadas (opcional)
├── module-federation.manifest.json          # dev
├── module-federation.manifest.staging.json  # staging
├── module-federation.manifest.prod.json     # produção
└── nx.json

What really matters

Module Federation solves technical problems of federated loading. Nx solves technical problems of build and dependency. Neither solves the real problem: microfrontends are a solution for Conway's Law, not for build performance.

Understand the role of Module Federation and Nx

Module Federation focuses on federated loading, while Nx manages builds and dependencies. No tool solves the intrinsic complexity of microfrontends.

Microfrontends are effective for teams that need to work independently, provided that business domains are well-defined.

Without this clarity, the project suffers from distributed complexity and the coupling typical of monoliths, harming the final result.

If your problem is that two teams cannot work on the same code without stepping on each other's toes, microfrontends can help — but only if business domains have clear enough boundaries so that remotes are genuinely independent.

Most projects that adopt microfrontends without this domain clarity end up with the worst of both worlds: the complexity of distributed architecture with the coupling of a monolith.

The policy in one sentence Microfrontends are for teams, not for code. If the driver is "we need different teams to deploy with different cadences in clear business domains" — use MFE. If the driver is "our application has become large" — use Angular lazy modules with Nx incremental builds. The second option solves 80% of the problems with 20% of the complexity.

The independence that microfrontends promise costs more than the tutorial shows. It is only worth paying when you know exactly what you are buying.


Sources

  • nx.dev/docs/technologies/module-federation — Official MFE architecture with Nx: when to use, version mismatch, affected deploys, shared libs strategy.

  • blog.angular.dev — Manfred Steyer (Feb/2025) — "Micro Frontends with Angular and Native Federation" — Module Federation vs Native Federation, singleton sharing, strictVersion, ESM + import maps.

  • DEV Community — Vitalii Petrenko (May/2025) — "Microfrontends in 2025: A Reality Check from the Trenches" — CSS isolation, state management cross-MFE, version governance.

  • infinum.com (Dec/2025) — "Implementing Micro Frontends — What to Look Out For" — shared packages, Angular updates in monorepo.

  • angulararchitects.io — "Multi-Framework and -Version Micro Frontends with Module Federation" — requiredVersion strategies, async bootstrap.

  • javascript-conference.com (Jul/2024) — "Microfrontends in the Monorepo" — nx affected:apps for selective deployment, enforce-module-boundaries.