Todo tutorial enseña a configurar microfrontends en minutos, pero pocos revelan el caos arquitectónico que surge tras meses de deploy en producción.

Todo tutorial muestra cómo configurar el Module Federation en 20 minutos. Ninguno muestra lo que sucede 6 meses después en producción: el CSS que se filtra entre remotes, la versión de Angular que tira abajo el shell, el estado compartido que hizo imposible el deploy independiente y el costo organizacional que el diagrama arquitectónico no captura.
El tutorial termina donde el problema comienza.
La mayoría de los equipos que implementan microfrontends llegan a producción en 3 meses y pasan los siguientes 9 meses resolviendo problemas que ningún tutorial mencionó.
El modelo es seductor: equipos independientes, deploys independientes, autonomía de tecnología. En la práctica, la independencia prometida tiene un costo que solo aparece cuando un equipo intenta subir una versión de Angular mientras otro aún está en el ciclo anterior. O cuando el CSS del remote de checkout comienza a afectar el layout del shell. O cuando descubres que el estado compartido que pusiste en un servicio global hizo que el deploy independiente fuera técnicamente imposible.
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
→ El número que importa En una encuesta con más de 20 proyectos de microfrontend de 2021-2023, el patrón era consistente: 3 meses para implementar, 9 meses para estabilizar. Los problemas de state compartido, CSS leaking y version mismatch no aparecen en demos; aparecen cuando los equipos comienzan a trabajar de forma genuinamente independiente.
La arquitectura de microfrontends no falla en el código. Falla en las fronteras que no definiste.
Lo que sucede 6 meses después del deploy.
Problema 01 — Version mismatch — el crash silencioso: El equipo A usa Angular 21. El equipo B actualiza a Angular 22 con la nueva API de Signal Forms. El shell carga ambos. El Module Federation intenta reconciliar las versiones con singleton: true. A veces funciona. A veces el remote B falla sin un mensaje de error obvio.
Problema 02 — CSS leaking — el estilo que no tiene fronteras: El remote de checkout define .btn { background: red } sin scope. El shell tiene .btn { background: blue }. Dependiendo del orden de carga, uno sobrescribe al otro. En producción, el orden cambia con el caching. El bug es no determinista.
Problema 03 — Estado global — el deploy que no es independiente: Para compartir el usuario autenticado, alguien puso el estado en un NgRx Store compartido entre todos los remotes. Ahora, cualquier cambio en el shape del estado exige un deploy coordinado de todos los remotes. El deploy "independiente" dejó de ser independiente.
Problema 04 — DX en desarrollo — 4 terminales para 1 feature: Para ejecutar el feature de checkout localmente, el desarrollador necesita iniciar shell, catalog-mfe, admin-mfe y checkout-mfe. El tiempo de setup local se triplicó. La autonomía del equipo vino con un overhead de DX que nadie contabilizó.
Problema 05 — Bundle size — el intercambio que no compartió: Cada remote empaquetó su propia copia de Angular Material porque la configuración de shared no incluyó las entradas secundarias (@angular/material/button, @angular/material/dialog). El bundle total quedó 3 veces más grande que el monolito original.
El Module Federation permite versiones diferentes. Nx, por defecto, no lo permite, y esa restricción es un feature, no un bug.
// ❌ 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 }, },};// ✅ Governança de versão explícita com Nx// libs/shared-mf-config/base.config.tsimport { 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' }, }};# Ver quais apps precisam ser (re)deployados após mudança em libs/authnx 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@22nx migrate --run-migrations# Aplica migrações em TODOS os apps do workspace de uma vez⚠️ La regla de Angular en monorepo Nx Cuando actualizas Angular en un workspace Nx, todas las apps del workspace se actualizan juntas. Esto es intencional; la alternativa es el infierno de versiones mixtas. Si dos equipos necesitan versiones diferentes de Angular, necesitan repositorios separados, y ahí pierden las ventajas del monorepo Nx.
El ViewEncapsulation de Angular aísla estilos de componentes pero no resuelve los estilos globales que cada remote define.
/* ❌ 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 *//* ✅ 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 */✅ La regla de oro de CSS en MFEs El shell es el único dueño de los estilos globales. Los remotes consumen CSS Custom Properties definidas por el shell. Ningún remote define valores absolutos de z-index — ese es el bug más difícil de depurar en producción, porque z-index depende del contexto de apilamiento y el shell no controla el contexto del remote.
⚠️ El antipatrón más común en MFEs Colocar el estado del usuario autenticado en un NgRx Store compartido vía Module Federation entre todos los remotes. Parece correcto: "es estado global, debe estar en el NgRx global". El problema es que cualquier cambio en el shape de ese estado exige que todos los remotes sean actualizados y desplegados al mismo tiempo. El deploy "independiente" deja de ser independiente.
// libs/core/src/lib/event-bus.service.ts// ✅ O Event Bus é o contrato entre remotes — não o shape do estadoimport { 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// ✅ Se você PRECISA de estado compartilhado, compartilhe o mínimoexport 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}// 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"}# ✅ 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 livenx serve shell --devRemotes=checkout,catalog# Com Dynamic Module Federation — aponta para staging:# O dev desenvolve checkout localmente, catalog e admin vêm do stagingcp src/assets/module-federation.manifest.staging.json \ src/assets/module-federation.manifest.json→ El patrón Nx recomendado para DX Configura un target
serve-with-staging-remotesen elproject.jsondel shell. El desarrollador inicia una única terminal, el shell carga el remote local que está desarrollando y los otros remotes del staging. Ningún mock, ningún stub; entorno real con un remote vivo.
// ❌ Shared incompleto — cada remote tem sua própria cópia do Materialshared: { '@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}// ✅ shareAll com entradas secundárias — a configuração corretaimport { shareAll } from '@nx/angular/module-federation';// shareAll compartilha AUTOMATICAMENTE todos os pacotes do package.json// Incluindo entradas secundárias como @angular/material/buttonexport 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✅ Nx bundle analysis integrada Ejecuta
nx build checkout --analyzepara abrir el Webpack Bundle Analyzer e identificar qué paquetes se están duplicando entre el shell y los remotes. Haz este análisis antes del primer deploy en producción — es mucho más difícil corregir después de que el caché del CDN se haya propagado.
Criterio | Microfrontends | Monolito modular con Nx |
|---|---|---|
Tamaño del equipo | ✅ 5+ equipos paralelos | ✅ 1-3 equipos — el costo de MFE no compensa |
Cadencia de deploy | ✅ Equipos con cadencias diferentes | ✅ Misma cadencia — el deploy unificado es más simple |
Dominios de negocio | ✅ Dominios genuinamente independientes | ❌ Mucha interdependencia — MFE crea fronteras artificiales |
Bundle inicial | ❌ Mayor — overhead de Module Federation | ✅ Menor |
Complejidad de config | ❌ Alta — versiones, CSS, estado, DX | ✅ Baja |
DX local | ⚠️ Degradada sin configuración cuidadosa | ✅ Simple — |
→ La recomendación de Nx La documentación oficial de Nx es directa: "Si quieres optimizar builds y no necesitas deploys independientes, usa nuestra guía de Faster Builds con Module Federation." Puedes tener builds incrementales, caché inteligente y repositorio único sin la complejidad de remotes desplegados independientemente. Esa es la respuesta correcta para la mayoría de los equipos de hasta 30 personas.
✅ 5+ equipos que necesitan desplegar con cadencias genuinamente diferentes
✅ Dominios de negocio claros que no comparten estado más allá de la autenticación
✅ SLAs independientes — un fallo en el checkout no debe afectar al catalog
✅ Design system establecido vía CSS Custom Properties antes de comenzar
✅ Gobernanza de versiones — política clara para actualizaciones de Angular
❌ Equipo único que "va a crecer" — anticípate con módulos lazy, no con MFE
❌ Complejidad técnica como objetivo — MFE es para problemas organizacionales
⚠️ Estado compartido extenso entre los remotes — rediseña los límites antes
⚠️ SEO crítico — los MFEs con Module Federation son CSR por defecto
Cada paso a continuación mitiga un riesgo real de producción. Saltar uno no genera un error inmediato, genera el problema que depurarás en producción 6 meses después.
Paso 1 — Crear el workspace Nx con preset Angular:
npx create-nx-workspace@latest minha-org --preset=angular-monorepoRiesgo mitigado: sin el preset correcto, el workspace no configura el @nx/enforce-module-boundaries automáticamente. Sin esta regla de lint, los remotes importarán unos de otros directamente, creando un acoplamiento que hace imposible el deploy independiente. Solo descubrirás el problema cuando intentes desplegar el checkout sin el catalog y el build falle.
Paso 2 — Generar el shell con Dynamic Module Federation:
nx g @nx/angular:host shell --remotes=checkout,catalog,admin --dynamic=trueRiesgo mitigado: sin --dynamic=true, la URL de cada remote queda hardcoded en el bundle del shell en tiempo de build. Cambiar la URL de un remote en producción exige rebuild y redeploy de todo el shell, destruyendo la independencia de deploy. Con el manifest JSON dinámico, cambias la URL en runtime sin tocar el shell.
Paso 3 — Crear las libs compartidas antes de cualquier feature:
nx g @nx/angular:lib libs/uinx g @nx/angular:lib libs/authnx g @nx/angular:lib libs/coreRiesgo mitigado: crear libs después de que los remotes ya existen significa que cada remote ya tiene su propia versión de componentes y servicios. Unificar después es una refactorización dolorosa a la que los equipos se resisten durante meses. Crear antes fuerza la disciplina de "código compartido va a libs" desde el primer commit; el acoplamiento nunca llega a existir.
Paso 4 — Configurar la base de shared config en libs/shared-mf-config:
// shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' })Riesgo mitigado: sin una config base centralizada, cada remote define su propio shared — e invariablemente alguien olvida incluir las entradas secundarias de Angular Material o del CDK. El resultado es el Problema 05: bundle 3 veces mayor. Con la config base en el monorepo, un único PR corrige todos los remotes al mismo tiempo.
Paso 5 — Establecer el Design System en el shell antes del primer remote: Define todas las CSS Custom Properties en shell/src/styles.scss y documenta la convención de namespace (ej: .checkout-mfe { ... }).
Riesgo mitigado: sin tokens CSS centralizados, cada remote define sus propios valores de color, espaciado y z-index. En 3 meses tienes 4 sistemas de diseño divergentes y CSS leaking no determinista (Problema 02). Refactorizar esto después significa convencer a 4 equipos de dejar de entregar features para alinear variables CSS — conversación que nunca sucede.
Paso 6 — Implementar el EventBusService antes del primer flujo cross-remote: libs/core/src/lib/event-bus.service.ts con tipos de evento en libs/core/src/lib/events.ts.
Riesgo mitigado: sin un contrato de comunicación establecido, el primer desarrollador que necesite pasar datos entre remotes pondrá estado en el NgRx global, creando el Problema 03. El EventBus obliga al equipo a pensar en contratos versionados antes de crear la dependencia de estado. Es mucho más fácil establecer el patrón correcto antes del primer flujo que eliminar estado global después de que 3 equipos dependan de él.
# Estrutura de workspace que aguenta 2 anos de produçãominha-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.jsonEl Module Federation resuelve problemas técnicos de carga federada. Nx resuelve problemas técnicos de build y dependencia. Ninguno de los dos resuelve el problema real: los microfrontends son una solución para la Conway's Law, no para el rendimiento de build.
Si tu problema es que dos equipos no pueden trabajar en el mismo código sin pisarse, los microfrontends pueden ayudar, pero solo si los dominios de negocio tienen fronteras lo suficientemente claras para que los remotes sean genuinamente independientes.
La mayoría de los proyectos que adoptan microfrontends sin esa claridad de dominio terminan con lo peor de ambos mundos: la complejidad de la arquitectura distribuida con el acoplamiento del monolito.
La política en una frase Los microfrontends son para equipos, no para código. Si el driver es "necesitamos que equipos diferentes desplieguen con cadencias diferentes en dominios de negocio claros", usa MFE. Si el driver es "nuestra aplicación se volvió grande", usa módulos lazy de Angular con Nx incremental builds. La segunda opción resuelve el 80% de los problemas con el 20% de la complejidad.
La independencia que los microfrontends prometen cuesta más de lo que el tutorial muestra. Solo vale la pena pagarlo cuando sabes exactamente qué estás comprando.
nx.dev/docs/technologies/module-federation — Arquitectura oficial de MFE con Nx: cuándo usar, version mismatch, deploys afectados, estrategia de shared libs.
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 (dic/2025) — "Implementing Micro Frontends — What to Look Out For" — shared packages, actualización de Angular en monorepo.
angulararchitects.io — "Multi-Framework and -Version Micro Frontends with Module Federation" — requiredVersion strategies, bootstrap asíncrono.
javascript-conference.com (jul/2024) — "Microfrontends in the Monorepo" — nx affected:apps para deploy selectivo, enforce-module-boundaries.