Los micro-frontends son uno de esos patrones arquitectónicos que suenan convincentes en conferencias y se sienten diferentes una vez que convives con ellos en producción. He trabajado dentro de una arquitectura de micro-frontends en un proyecto Angular empresarial en BMO, y tengo una opinión considerada al respecto. No es una respuesta simple, pero es honesta.
La versión corta: los micro-frontends resuelven problemas reales para equipos de un tamaño y estructura organizacional específicos. Para equipos menores a ese umbral, agregan complejidad que no produce beneficio. El desafío es que el umbral es más alto de lo que la mayoría de las discusiones arquitectónicas sugieren, y los costos son más concretos que los beneficios.
Qué Problema Realmente Resuelven los Micro-Frontends
El argumento honesto a favor de los micro-frontends no es sobre tecnología. Es sobre topología de equipos. Cuando múltiples equipos necesitan desarrollar, desplegar y ser dueños de diferentes partes de una gran aplicación frontend de manera independiente, una app Angular monolítica crea costos de coordinación. El Equipo A no puede desplegar sus cambios sin coordinarse con el Equipo B. Un bug en el código del Equipo C puede bloquear un release para todos los demás equipos. La propiedad es difusa porque todos son dueños del mismo codebase.
Los micro-frontends abordan esto trazando límites duros a nivel de despliegue y propiedad. El Equipo A es dueño del MFE-A. Pueden desplegarlo independientemente. Sus bugs no afectan el despliegue del Equipo B. Sus decisiones tecnológicas (dentro de ciertos límites) no restringen al Equipo B. El problema de coordinación del monolito desaparece porque no hay un monolito compartido.
Si no tienes este problema, si tienes un equipo, o dos equipos estrechamente coordinados, o un codebase que se despliega infrecuentemente, los micro-frontends no resuelven nada que realmente tengas. Introducen costos sin el beneficio correspondiente.
Configuración de Module Federation en Angular
El camino más común hacia micro-frontends en Angular es Module Federation a través del paquete @angular-architects/module-federation, mantenido por la comunidad; Angular en sí no incluye ninguna solución oficial de micro-frontends. Este plugin de webpack permite que una aplicación host cargue módulos JavaScript remotos en tiempo de ejecución desde URLs desplegadas separadamente. Cada módulo remoto es una aplicación Angular desplegada independientemente que expone componentes, rutas o servicios.
La configuración implica modificar la configuración de webpack tanto para el host como para cada remoto. Module Federation es una funcionalidad de webpack, así que necesita el builder de Angular basado en webpack en lugar del builder por defecto basado en esbuild que llegó en Angular 17 (Native Federation, de los mismos mantenedores, cubre el camino de esbuild). El plugin genera las configuraciones de webpack por ti. En la práctica, las configuraciones generadas necesitan personalización para cualquier cosa no trivial.
// webpack.config.js for a remote MFE
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const { share } = require('@angular-architects/module-federation/webpack');
module.exports = {
output: {
uniqueName: 'workspaceApp',
publicPath: 'auto'
},
optimization: { runtimeChunk: false },
experiments: { outputModule: true },
plugins: [
new ModuleFederationPlugin({
library: { type: 'module' },
name: 'workspaceApp',
filename: 'remoteEntry.js',
exposes: {
'./WorkspaceRoutes': './src/app/workspace/workspace.routes.ts'
},
shared: share({
'@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'@angular/common': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'@angular/router': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'rxjs': { singleton: true, strictVersion: false, requiredVersion: 'auto' }
})
})
]
};// app.routes.ts in the host
import { Routes } from '@angular/router';
import { loadRemoteModule } from '@angular-architects/module-federation';
import { environment } from '../environments/environment';
export const routes: Routes = [
{
path: 'workspace',
loadChildren: () =>
loadRemoteModule({
type: 'module',
remoteEntry: environment.workspaceRemoteUrl + '/remoteEntry.js',
exposedModule: './WorkspaceRoutes'
}).then(m => m.WORKSPACE_ROUTES)
}
];Algo que conviene decir claro, porque este post es de agosto de 2026: el camino del builder de webpack de arriba es el legacy. El builder por defecto de Angular es basado en esbuild desde la v17, y la opción mantenida para él es @angular-architects/native-federation. Los conceptos se trasladan: remotos, un remote entry, singletons compartidos. Lo que cambia es el cableado. Native Federation usa un archivo de configuración de federación más una llamada a initFederation en main.ts contra un manifiesto JSON, en lugar de una config de webpack. Empieza ahí un micro-frontend nuevo. Muestro la config de webpack porque es lo que todavía corre la mayoría de los setups enterprise existentes, y porque migrar es un proyecto aparte.
Comunicación Entre MFEs: La Parte Más Difícil
Una vez que tienes múltiples aplicaciones Angular desplegadas independientemente que necesitan comunicarse, descubres que el sistema de inyección de dependencias de Angular no fue diseñado para esto. Los servicios en la aplicación host no están automáticamente disponibles para los servicios en aplicaciones remotas. No puedes inyectar un servicio del host en un componente remoto directamente.
Hay tres patrones para comunicación entre MFEs, cada uno con tradeoffs significativos.
El primer patrón son servicios singleton compartidos. Expones un servicio del host (o de una biblioteca compartida) y configuras Module Federation para tratarlo como una dependencia compartida singleton. Los MFEs remotos importan e inyectan este servicio. Funciona, pero crea acoplamiento fuerte. El remoto ahora tiene una dependencia de compilación con la interfaz del servicio del host. Los cambios al servicio compartido requieren coordinar todos los MFEs que lo consumen.
El segundo patrón son eventos personalizados. Los MFEs se comunican despachando y escuchando CustomEvents del navegador en window. Esto es completamente desacoplado, sin imports compartidos. Funciona a través de cualquier tecnología, no solo Angular. La desventaja es que los CustomEvents son de tipo string, no tienen soporte TypeScript por defecto, y debuggear requiere entender una cadena de eventos que atraviesa múltiples límites de aplicación.
El tercer patrón es un store de estado compartido, típicamente un store mínimo sin framework, TypeScript puro sin imports de Angular, sin NgRx y sin signals, compartido por referencia a través del mecanismo de dependencia compartida de Module Federation. Tanto host como remotos leen y escriben en este store. Es una solución de gestión de estado global, que tiene toda la complejidad del estado global a escala.
// shared-mfe-store.ts
// A framework-free store, shared as a singleton through Module Federation.
// No Angular imports live in this file.
import type { AppNotification, User, Workspace } from './shared-types';
export interface SharedState {
currentUser: User | null;
activeWorkspace: Workspace | null;
notifications: AppNotification[];
}
export type SharedAction =
| { type: 'SET_USER'; user: User | null }
| { type: 'SET_WORKSPACE'; workspace: Workspace | null };
export class SharedMFEStore {
private _state: SharedState = this.initialState();
private _listeners = new Set<(state: SharedState) => void>();
getState(): SharedState {
return { ...this._state };
}
dispatch(action: SharedAction): void {
this._state = this.reduce(this._state, action);
this._listeners.forEach(listener => listener({ ...this._state }));
}
// Returns the unsubscribe function. Callers must keep it and call it.
subscribe(listener: (state: SharedState) => void): () => void {
this._listeners.add(listener);
return () => {
this._listeners.delete(listener);
};
}
private initialState(): SharedState {
return { currentUser: null, activeWorkspace: null, notifications: [] };
}
private reduce(state: SharedState, action: SharedAction): SharedState {
switch (action.type) {
case 'SET_USER': return { ...state, currentUser: action.user };
case 'SET_WORKSPACE': return { ...state, activeWorkspace: action.workspace };
default: return state;
}
}
}
export const sharedStore = new SharedMFEStore();El store en sí se mantiene libre de Angular. El puente hacia Angular es un servicio aparte, y es ahí donde hay que manejar el teardown: subscribe devuelve una función de desuscripción, y si nadie la llama, el store se queda con una referencia a un componente que ya no existe.
// shared-state-bridge.service.ts
import { DestroyRef, Injectable, Signal, inject, signal } from '@angular/core';
import { sharedStore, SharedState } from 'shared/shared-mfe-store';
@Injectable({ providedIn: 'root' })
export class SharedStateBridge {
private destroyRef = inject(DestroyRef);
select<T>(selector: (state: SharedState) => T): Signal<T> {
const sig = signal<T>(selector(sharedStore.getState()));
const unsubscribe = sharedStore.subscribe(state => sig.set(selector(state)));
this.destroyRef.onDestroy(unsubscribe);
return sig.asReadonly();
}
}Desafíos de Estado Compartido
El desafío de estado compartido con MFEs es que el estado que necesitas compartir es casi siempre el estado que cambia más frecuentemente y tiene más consumidores. Estado de autenticación, usuario actual, notificaciones globales: estas son exactamente las cosas que cada MFE necesita y que más necesitas acertar.
El problema se compone con Angular Signals. Los signals solo cruzan una frontera de MFE cuando @angular/core se comparte como singleton, como en la config de arriba, y cuando el signal mismo se entrega a través de una biblioteca o un store compartido. Si un remoto termina con su propia copia de Angular, los signals del host le son invisibles. Tienes que hacer un puente entre el sistema reactivo nativo de Angular y cualquier mecanismo de comunicación entre fronteras que hayas elegido. Este código de puente es frágil, difícil de testear, y requiere comprensión profunda de cómo la detección de cambios de Angular interactúa con cambios de estado externos.
No es raro que un proyecto gaste más tiempo de ingeniería en la infraestructura de comunicación de MFEs que en las funcionalidades que esos MFEs entregan. Cuando la plomería toma más esfuerzo que las funcionalidades, probablemente sobre-arquitecturaste.
Complejidad de Despliegue
Una app Angular monolítica tiene un artefacto de despliegue: un output de build que va a un CDN. Una arquitectura micro-frontend con cinco MFEs tiene cinco artefactos de despliegue, cinco rutas de CDN, gestión de versiones entre los cinco, y una aplicación host que debe referenciar URLs remotas que funcionen en cada entorno.
El problema de configuración de URLs es genuinamente difícil. En desarrollo, los remotos corren en localhost:4201, 4202, etc. En staging, viven en subdominios diferentes. En producción, otros diferentes. La aplicación host debe conocer la URL correcta para cada remoto en cada entorno. Necesitas configuración consciente del entorno que se empaqueta con el host pero referencia remotos desplegados externamente: un problema de configuración que no existe en un monolito.
La compatibilidad de versiones entre host y remotos es otro problema real. Si actualizas una biblioteca compartida en el host, todos los remotos que declaran esa biblioteca como dependencia compartida recibirán la versión del host en tiempo de ejecución. Si la biblioteca compartida tiene un breaking change y un remoto fue construido contra la versión anterior, tienes un conflicto de versiones en runtime. La versión de Angular, la versión de RxJS, y cualquier biblioteca de design system compartida se convierten en puntos potenciales de conflicto en runtime.
Tradeoffs de Autonomía de Equipos: La Evaluación Honesta
El beneficio prometido de los MFEs es la autonomía de equipos. Cada equipo despliega independientemente, es dueño completo de su MFE, y no está bloqueado por otros equipos. En la práctica, la autonomía es más limitada de lo anunciado, por dos razones.
Primero, las dependencias compartidas restringen las decisiones tecnológicas. Si el host usa Angular 21, todos los remotos deben usar una versión compatible de Angular. No puedes tener un equipo en Angular 19 y otro en Angular 21 en el mismo ecosistema MFE: la restricción de biblioteca compartida singleton hace que la divergencia de versiones produzca errores en runtime. Los equipos son autónomos de ciertas maneras, pero están acoplados a nivel de dependencias.
Segundo, las preocupaciones transversales requieren coordinación. Cambios en autenticación, cambios en navegación global, actualizaciones del design system compartido: estos requieren coordinación entre equipos independientemente de la arquitectura MFE. El límite arquitectónico no hace que la comunicación organizacional desaparezca. La mueve de code review a contratos de interfaces y testing de integración.
El beneficio de autonomía es real, pero es más estrecho de lo que el discurso sugiere. Obtienes despliegue autónomo de funcionalidades que viven completamente dentro del límite del MFE de un equipo. Para funcionalidades que cruzan límites, los MFEs no ayudan y pueden hacer las cosas más difíciles.
Dónde Tiene Sentido
Quiero ser justo con el patrón. Imagina una organización grande donde múltiples equipos de desarrollo trabajan en diferentes productos que comparten una experiencia de portal unificada. El Equipo A es dueño del MFE de onboarding. El Equipo B es dueño del MFE de gestión de cuentas. El Equipo C es dueño del MFE de reportes. Estos equipos tienen diferentes cadencias de release, diferentes product managers y diferentes rotaciones de guardia. No deberían estar bloqueados por los despliegues de los otros. Ahí es donde los MFEs tienen sentido genuino.
La superficie compartida entre estos MFEs es intencionalmente estrecha: un design system, estado de autenticación, y un marco de navegación global. Todo dentro de cada área de producto es autónomo. El límite de despliegue coincide con el límite organizacional. Esa alineación es la clave: cuando la arquitectura coincide con la estructura organizacional, los MFEs reducen fricción real.
Dónde Es Sobre-Ingeniería
Un error común es aplicar arquitectura MFE a una aplicación donde un solo equipo pequeño es dueño de cada micro-frontend y toma cada decisión de despliegue. No hay problema de independencia de despliegue que resolver cuando un equipo toma todas las decisiones de despliegue. No hay problema de autonomía de equipos cuando todos están en el mismo stand-up. En ese escenario, la arquitectura MFE agrega meses de complejidad de configuración, overhead de mantenimiento continuo, y situaciones difíciles de debugging, todo por intentar resolver un problema organizacional que no existe.
- Un solo equipo de menos de 8 desarrolladores: un monorepo con límites internos fuertes es casi siempre mejor
- Aplicaciones con menos de 5 áreas de producto distintas: el overhead de unidades de despliegue no vale la pena
- Equipos que lanzan menos de dos releases por semana: el beneficio de independencia de despliegue es mínimo
- Proyectos greenfield sin product-market fit probado: vas a pivotar la arquitectura antes de estabilizar el producto
- Proyectos donde la superficie compartida entre MFEs es grande: el acoplamiento fuerte a través de estado compartido anula la independencia
Mi Veredicto
Los micro-frontends con Angular Module Federation son una solución legítima a un problema real. Ese problema es: múltiples equipos que necesitan verdadera independencia de despliegue para funcionalidades que tienen una superficie compartida pequeña. Si esa descripción encaja con tu situación precisamente, los MFEs valen la inversión.
Para mi proyecto SaaS personal, nunca usaría MFEs. Desarrollador solo, un despliegue, sin problema de coordinación de equipos. Para un startup con un producto y tres desarrolladores, no usaría MFEs. Para una organización grande con equipos de producto verdaderamente independientes que comparten un portal, son la decisión correcta.
El error que veo más frecuentemente es aplicar MFEs porque suenan maduros y escalables, no porque un problema concreto que los MFEs resuelven esté realmente presente. La arquitectura debería ser una respuesta a problemas reales, no una optimización preventiva para problemas que podrías tener algún día. Si no tienes múltiples equipos bloqueados por los despliegues de los otros ahora mismo, invierte ese tiempo de ingeniería en entregar funcionalidades.
La arquitectura correcta no es la más sofisticada. Es la más simple que resuelve los problemas que realmente tienes, con suficiente estructura para crecer hacia los problemas que probablemente encontrarás.
Referencias
- Webpack Module Federation. https://webpack.js.org/concepts/module-federation/
- @angular-architects/module-federation. https://www.npmjs.com/package/@angular-architects/module-federation
- Nx micro-frontend architecture. https://nx.dev/concepts/module-federation/micro-frontend-architecture