wandres.dev
MODULE FEDERATION · microfrontends

Los trade-offs: acoplamiento y monolito modular

La federación no es gratis. Compartir dependencias reintroduce, por la puerta de atrás, el acoplamiento de versiones que los microfrontends prometían eliminar. Los fallos cruzan fronteras de despliegue y la depuración se vuelve distribuida. Y muchas veces la respuesta correcta es un monolito modular con fronteras bien impuestas. Cuándo federar y cuándo no.

⏱ 18 min

Este nivel cierra con la lección más importante y la que más se omite: la federación se paga. Compartir dependencias, que resolvía la duplicación, reintroduce por la puerta de atrás el acoplamiento de versiones que los microfrontends prometían abolir. Los fallos dejan de detectarse en el build y aparecen en runtime, cruzando fronteras de despliegue que vuelven la depuración distribuida y difícil. Y muy a menudo, la arquitectura correcta no es federar, sino un monolito modular con fronteras internas bien impuestas.

🎯 Al terminar esta lección sabrás
  • Reconocer el acoplamiento de versiones que shared reintroduce pese a la promesa de independencia.
  • Entender por qué la depuración se vuelve distribuida y qué la hace más costosa.
  • Diseñar defensas de runtime: fallbacks, aislamiento de fallos y observabilidad entre remotes.
  • Decidir con criterio cuándo un monolito modular es la mejor arquitectura.

El acoplamiento de versiones que nadie ve

La promesa de los microfrontends es la independencia: cada equipo despliega a su ritmo. La letra pequeña es shared. En el instante en que host y remotes comparten React como singleton, quedan atados a un rango de versiones mutuamente compatible. Ningún equipo puede saltar por su cuenta a la próxima major del framework: hacerlo rompería la única instancia que todos comparten. La independencia de despliegue convive, así, con una dependencia de versión que no aparece en ningún diagrama.

Es un acoplamiento especialmente traicionero porque es invisible hasta que muerde. Todo funciona mientras las versiones encajan; el día que un equipo necesita una major nueva e incompatible, descubre que su despliegue supuestamente autónomo requiere una migración coordinada de todos los demás. El acoplamiento estaba ahí desde el principio, latente en el contrato de shared, esperando la actualización que lo hiciera visible.

⚠️
La independencia de despliegue no es independencia de versiones

Que cada equipo despliegue cuando quiera no significa que cada equipo elija sus versiones cuando quiera. Toda dependencia en shared como singleton es un punto donde los equipos deben acordar un rango común y migrar juntos cuando ese rango se rompe. Cuantas más dependencias compartes, más rígido es ese tratado. La independencia real es inversamente proporcional al tamaño de tu ámbito compartido.

Depuración distribuida

En una aplicación única, un error tiene un stack trace que apunta a una línea de un build que controlas. En una federada, un error puede nacer en un remote construido por otro equipo, con otro pipeline, otros sourcemaps y otro calendario de despliegue, y manifestarse dentro del árbol del host. El rastro cruza una frontera de despliegue, y con ella se pierde buena parte del contexto: no siempre tienes los sourcemaps del remote, ni sabes qué versión estaba desplegada cuando el usuario falló.

A esto se suma un modo de fallo nuevo: un remote caído o incompatible no rompe un build, rompe una pantalla en producción. Una URL que devuelve 404, un manifiesto corrupto, una versión que dejó de negociar con el ámbito compartido: nada de esto existía en el monolito, donde lo que se desplegaba junto se compilaba junto. La federación te obliga a programar defensivamente contra la ausencia o el fallo de piezas que no controlas.

import { loadRemote, registerPlugins } from "@module-federation/runtime";

// Un plugin de runtime intercepta el fallo de un remote y degrada con gracia.
registerPlugins([
  {
    name: "fallback",
    errorLoadRemote({ id, error }) {
      reportar(id, error);
      return { default: () => renderMensajeDeGracia() };
    },
  },
]);

const modulo = await loadRemote("catalogo/Panel");
💡
Trata cada frontera de remote como un límite de fallo

Todo punto donde cargas un remote es un sitio donde algo ajeno puede fallar en runtime. Envuélvelo: un error boundary alrededor del componente remoto, un errorLoadRemote que degrade a un fallback, y telemetría que registre qué remote y qué versión fallaron. La federación sin estas defensas convierte el fallo de cualquier equipo en la caída de la pantalla de todos.

flowchart TD
U[usuario ve un error] --> Q{el fallo nacio en el host o en un remote}
Q -->|host| H[stack y sourcemaps propios]
Q -->|remote| R[otro build, otros sourcemaps, otra version]
R --> N[hace falta telemetria que cruce la frontera]
style U fill:#f38ba8,color:#11111b
style H fill:#a6e3a1,color:#11111b
style R fill:#fab387,color:#11111b
style N fill:#f9e2af,color:#11111b

Cuándo un monolito modular gana

Aquí está la conclusión que casi nadie pone por escrito: para la mayoría de los productos, un monolito modular es mejor que una federación. Un monolito modular es una sola aplicación, un solo despliegue, pero con fronteras internas rigurosas: módulos o paquetes de un workspace con límites que se imponen por herramienta —reglas de linter que prohíben importar a través de fronteras, referencias de proyecto de TypeScript, el grafo de un monorepo— en vez de por red.

Te da casi toda la modularidad de los microfrontends —código dividido por dominio, propiedad clara, fronteras explícitas— sin ninguno de sus impuestos de runtime: la integración vuelve a ocurrir en el build, así que el compilador verifica los contratos, no hay negociación de versiones que gestionar, no hay remotes que puedan caerse, y el stack trace vuelve a ser uno solo. Pierdes exactamente una cosa: el despliegue independiente. Si no lo necesitas de verdad, estás pagando toda la complejidad de la federación por un beneficio que no usas.

Monolito modular Microfrontends federados
Integración en build, verificada en runtime, negociada
Despliegue uno solo independiente por equipo
Versiones compartidas resueltas al compilar negociadas en runtime
Fallo de una pieza rompe el build rompe la pantalla
Depuración un stack, unos sourcemaps distribuida entre builds
Coste operativo bajo alto
📝
Empieza modular, federa solo cuando el despliegue lo exija

La ruta de menor arrepentimiento es casi siempre empezar por un monolito modular bien estructurado —fronteras claras, dominios separados, límites impuestos por herramienta— y federar únicamente aquellas fronteras donde aparezca una necesidad real y demostrable de despliegue independiente. Es mucho más fácil promover una frontera interna limpia a un remote federado que desenredar una federación prematura que nunca hizo falta. La modularidad es reversible; la federación, mucho menos.

La federación no elimina el acoplamiento: lo traslada del build al runtime y de la máquina a la organización

Si hay una sola idea que llevarse de todo este nivel, es que Module Federation no hace desaparecer el acoplamiento entre las partes de un sistema; lo traslada, y hay que decidir con los ojos abiertos si el sitio a donde lo lleva es mejor que donde estaba. En un monolito, el acoplamiento vive en el build: el compilador ve todas las piezas juntas, verifica cada contrato, y lo que no encaja rompe la compilación en la máquina de quien lo rompió, antes de llegar a nadie. La federación mueve ese mismo acoplamiento a dos lugares nuevos. Lo mueve al runtime, donde ya no hay compilador que verifique el contrato entre host y remote, y donde una incompatibilidad que antes rompía un build ahora rompe la pantalla de un usuario que no tiene culpa de nada. Y lo mueve a la organización, porque el ámbito compartido convierte lo que era una relación entre módulos en un tratado entre equipos que deben acordar versiones y migrar coordinados. A cambio de ese doble traslado obtienes algo valioso y concreto —el despliegue independiente— que para una organización con muchos equipos autónomos puede justificar de sobra el precio. Pero el error recurrente de la industria es adoptar la federación por sus virtudes técnicas aparentes —cargar código en runtime suena potente y moderno— sin contabilizar que cada gramo de esa potencia se paga en fallos que se detectan tarde, en depuración que cruza fronteras, en versiones que hay que negociar entre equipos que preferirían no hablarse. La pregunta correcta nunca es si la federación es potente; lo es. La pregunta es si el problema que tienes es un problema de despliegue independiente entre equipos, porque ese es el único problema para el que federar es la respuesta y no una complicación disfrazada de arquitectura. Si tu problema es cualquier otro —una app grande, un build lento, ganas de modularizar— la respuesta casi siempre es un monolito modular con fronteras bien impuestas, que te da la modularidad sin trasladar el acoplamiento a los dos peores sitios donde puede vivir.

⚔️ Decide con los ojos abiertos
  1. Enumera las dependencias de tu ámbito shared y, para cada una, escribe qué migración coordinada obligaría una major incompatible.
  2. Provoca la caída de un remote en una producción simulada y comprueba qué ve el usuario sin defensas; luego añade un error boundary y un errorLoadRemote.
  3. Reproduce un error originado en un remote y observa cuánto contexto pierdes al no tener sus sourcemaps.
  4. Toma un producto real y decide, con la tabla de esta lección, si justifica federación o si un monolito modular lo sirve mejor.
  5. Diseña una frontera interna limpia en un monolito modular que pudieras, el día de mañana, promover a remote sin reescribirla.