ADR 007: Browser CustomEvents para comunicacion entre MFEs
Estado: Aceptado Fecha: 2025-03-15
Contexto
Los micro frontends necesitan comunicarse para preocupaciones transversales: navegacion, cambios de contexto de usuario, actualizaciones del carrito, notificaciones. El mecanismo de comunicacion no debe crear acoplamiento entre MFEs y debe funcionar con ciclos de despliegue independientes.
Decision
Usar CustomEvents nativos del navegador despachados en window para la comunicacion entre MFEs, con contratos de eventos tipados definidos en un paquete TypeScript compartido (@org/event-contracts).
Consecuencias
Positivas
- Cero dependencias runtime: CustomEvent es una API nativa del navegador
- Acoplamiento debil: los MFEs publican/suscriben sin conocerse entre si
- Agnostico de framework: funciona independientemente de la version de React o del framework
- Type safety via tipos de contrato de eventos compartidos (en tiempo de compilacion, no en runtime)
- Depuracion sencilla: los eventos son visibles en el DevTools del navegador
- Sin problemas de versionado: no hay version de libreria compartida que coordinar
- API familiar: todo desarrollador web conoce addEventListener/dispatchEvent
- Facil de testear: straightforward para mockear en tests unitarios
Negativas
- Sin garantias de entrega integradas (fire-and-forget, sin acknowledgment)
- Sin replay de eventos: si un MFE se carga despues de que se despacho un evento, lo pierde
- Contaminacion del namespace global (mitigado con la convencion de prefijo
mfe:) - Sin mecanismo de back-pressure para eventos de alta frecuencia
- Type safety en runtime requiere validacion manual (TypeScript solo ayuda en tiempo de compilacion)
- Sin garantias integradas de ordenacion de eventos entre boundaries asincronos
Guia de versionado de eventos
Dado que los MFEs se despliegan de forma independiente y pueden consumir diferentes versiones del paquete @org/event-contracts, los eventos deben versionarse con cuidado para evitar fallos en runtime.
Campo de version en el payload
Cada payload de evento debe incluir un campo version en el nivel superior:
interface MfeEvent<T> {
version: number; // Integer, starting at 1
type: string; // Event name, e.g., "mfe:cart:item-added"
payload: T; // Event-specific data
timestamp: number; // Unix timestamp (ms)
}
Los consumidores deben verificar el campo version antes de procesar e ignorar eventos con versiones no reconocidas en lugar de lanzar errores.
Adiciones retrocompatibles (cambios menores)
Los siguientes cambios son retrocompatibles y no requieren un incremento de version:
- Agregar nuevos campos opcionales al payload. Los consumidores existentes que no leen el nuevo campo siguen funcionando.
- Agregar nuevos tipos de evento al paquete de contratos. Los MFEs que no se suscriben al nuevo tipo no se ven afectados.
- Ampliar un tipo union (p.ej., agregar un nuevo valor enum a un campo de estado), siempre que los consumidores manejen valores desconocidos con gracia mediante un caso por defecto.
Para adiciones retrocompatibles, el campo version permanece sin cambios. Actualizar el paquete @org/event-contracts con los nuevos tipos y publicar una nueva version minor del paquete.
Estrategia de migracion ante breaking changes
Un breaking change es cualquier modificacion que pueda causar que los consumidores existentes fallen:
- Eliminar o renombrar un campo
- Cambiar el tipo de un campo
- Cambiar el significado semantico de un campo existente
- Eliminar un tipo de evento
Cuando un breaking change es necesario:
- Incrementar el campo version en el payload del evento (p.ej.,
version: 1pasa aversion: 2). - Periodo de publicacion dual: El MFE productor debe emitir tanto la version antigua como la nueva del evento durante una ventana de migracion (recomendado: 2 ciclos de sprint o 4 semanas). Esto permite que los MFEs consumidores se actualicen de forma independiente.
- Migracion de consumidores: Cada MFE consumidor agrega un handler para la nueva version. Durante el periodo de publicacion dual, los consumidores deben aceptar ambas versiones. Una vez que todos los consumidores han migrado, el handler de la version antigua puede eliminarse.
- Aviso de deprecacion: Marcar los tipos de la version antigua como
@deprecateden el paquete@org/event-contracts. Agregar un console warning en modo desarrollo si un MFE aun emite la version antigua. - Retirar la version antigua: Tras confirmar que todos los consumidores han migrado (via telemetria o code review), eliminar la version antigua del MFE productor y de los tipos de contrato.
Buenas practicas de versionado
- Mantener los payloads de eventos pequenos y enfocados. Los payloads mas pequenos son mas faciles de evolucionar.
- Preferir cambios aditivos sobre modificaciones. Disenar payloads pensando en la extension.
- Documentar el esquema de cada version de evento en el README del paquete
@org/event-contracts. - Usar validacion en runtime (p.ej., Zod o una comprobacion de esquema ligera) en los boundaries de los consumidores para capturar payloads inesperados de forma temprana, especialmente durante ventanas de migracion.
Alternativas consideradas
Store compartido de Redux / Zustand
Consistencia fuerte y depuracion (Redux DevTools). Pero: crea acoplamiento fuerte entre MFEs, conflictos de version para la libreria de estado, el store compartido se convierte en un cuello de botella de coordinacion, rompe la desplegabilidad independiente. Un MFE actualizando su dependencia de Redux podria romper todos los demas MFEs.
Bus de eventos basado en RxJS / Observables
Composicion potente (debounce, buffer, combine). Pero: agrega una dependencia runtime compartida (la version de RxJS debe coincidir entre todos los MFEs), curva de aprendizaje pronunciada para algunos desarrolladores, excesivo para los patrones simples de pub-sub que necesitamos.
postMessage (basado en iframes)
Necesario para micro frontends con iframes. Pero: usamos Module Federation (misma ventana), overhead de serializacion de postMessage, API mas compleja que CustomEvents.
Libreria personalizada de event emitter
Mas funcionalidades (listeners wildcard, once, namespaces). Pero: agrega una dependencia runtime compartida en la que todos los MFEs deben coincidir. CustomEvents son suficientes y libres de dependencias.