Una app federada dibujada como territorios de equipos distintos, unidos por finos raíles compartidos y una pequeña shell central

Quién es dueño de qué: fronteras, equipos y acoplamiento en una app React Native federada

Una app React Native normal nunca tiene esta discusión. Un equipo, un repo, un store. Una pantalla es una carpeta, un componente compartido es un import, y la pregunta de quién es dueño de una definición de datos tiene una respuesta tan obvia que nadie la dice en voz alta: el equipo, porque solo hay uno.

Los posts 4 a 6 de esta serie desmontaron ese arreglo a propósito: dos features de pestaña que se construyen y publican por su cuenta, una pantalla que viaja entre ellas como paquete versionado, un store que llena en runtime código que la shell no ha visto nunca. Cada paso obligó a una decisión de propiedad: dónde vive la pantalla compartida, quién puede tocar el store, dónde se sienta una definición de datos. Cada decisión recibió un párrafo en el momento en que el tutorial la tomó. Este post las reúne y las defiende de una vez, como un todo.

Está escrito como un conjunto de tests, no de veredictos. Cada sección expone el razonamiento y las condiciones bajo las que se sostiene; cambia las condiciones (número de equipos, confianza entre ellos, ritmo de releases, el tamaño del equipo de plataforma) y algunas respuestas cambian con toda honestidad. Llévate los tests, ponlos frente a tu propio organigrama, y discrepa de esta serie con precisión.

El primer test: ¿necesitas algo de esto?

Cada problema de este ensayo es comprado, no heredado. Una app de un solo equipo organizada feature-first ya dibuja las fronteras que importan: una carpeta por dominio, con las pantallas, hooks y llamadas a API del dominio dentro, y cada cruce visible en review como un import. Ese modelo no cuesta nada de mantener: ni registro, ni números de versión, ni un segundo dev server. Una frontera se redibuja en una tarde moviendo ficheros, y una emergencia la cruza con un import y un TODO.

La federación es lo que pasa cuando las carpetas se convierten en equipos. La debilidad del modelo de carpetas es el tren de releases, no las fronteras: cada carpeta sigue saliendo en un solo binario, en una sola fecha, retenida por la feature más lenta. Cuando equipos separados necesitan fechas de release separadas, la frontera de carpeta tiene que convertirse en frontera de despliegue, y todas las preguntas de este ensayo salen de ese ascenso.

Así que el primer test es el más barato, y bloquea el resto.

La puerta. ¿Publica esta app más de un equipo? Si no, quédate en las carpetas. Todos los tests de abajo asumen que la respuesta es sí.

El primer post de esta serie pesa el intercambio entero y aterriza donde este párrafo: la federación convierte fricción organizativa en maquinaria técnica, y una app sin la fricción paga una maquinaria que nunca usa.

Una frontera es un equipo, no una pantalla

Cuando la pantalla de detalle del Pokémon necesitó un hogar al que llegaran las dos pestañas, la respuesta federada se proponía sola: un tercer remote, en su propio puerto, declarado en los mapas de los dos consumidores. El post 5 le dio una audiencia justa: funciona mecánicamente, Module Federation resuelve remotes anidados sin quejarse, y la pantalla queda actualizable sin tocar a ningún consumidor. La respuesta siguió siendo no, en la frase de la que crece este ensayo entero: una frontera de federación es un desplegable, y los equipos cortan los desplegables por dominios.

El rechazo va de costes de funcionamiento. Un remote carga con costes que una carpeta no conoce: un dev server que mantener en marcha, un pipeline que mantener en verde, una versión que publicar, y un modo de fallo propio cuando su bundle no llega por la red. Esos costes son fijos por frontera, y un equipo los absorbe como gastos generales de un dominio que iba a poseer de todos modos. Una pantalla no tiene equipo detrás. Suelta una pantalla como desplegable propio y los gastos aparecen mientras el dueño no aparece nunca; concede ese estatus una vez y no queda ningún sitio con principios donde negárselo a la siguiente pantalla. El final de ese camino es una app por pantalla, y un proceso de release que es todo coordinación y nada de publicar.

cortada por dominio: un equipo por cajadominio Pokédextodas sus pantallasdominio partytodas sus pantallascortada por pantalla: sin dueñoslistdetailajustes

Así que la pantalla se publicó como @pokedex/detail: un paquete en un registro, instalado por las dos pestañas, montado por cada una dentro de su propio stack. Las pestañas siguen siendo los desplegables; la pantalla es una dependencia. Esto es la ley de Conway usada a propósito: si el sistema va a reflejar la estructura de comunicación de todos modos, dibuja las líneas de módulos donde ya están las líneas de equipos.

Test 1. ¿Tiene esta frontera un equipo detrás? El test corta en las dos direcciones: les niega remotes a las pantallas, y se los concede a los dominios que se han quedado pequeños en su carpeta.

Un componente compartido renderiza lo que le dan

Los micro-frontends web tienen un patrón respetable en el que un fragmento compartido llega listo: busca sus propios datos, gestiona sus propios estados de carga, y cae en cualquier host como una feature terminada. El atractivo es real. Un equipo guarda píxeles y datos detrás de una sola costura, los consumidores escriben una sola línea, y un arreglo de datos sale sin que ningún consumidor cambie código. Muchos montajes de micro-frontends web funcionan así con coherencia, al ritmo de despliegue de la web.

El precio está en lo que el fragmento tiene que cargar. Un componente que hace fetch carga con una opinión sobre cada capa que tiene debajo: un cliente HTTP, una caché, una política de reintentos, una historia de autenticación. Cada consumidor hereda esas opiniones sin verlas. Dos fragmentos así en una misma pantalla pueden discrepar de todo ello. En móvil la herencia pesa más, porque lo que el fragmento carga se duplica en un bundle que un teléfono tiene que descargar.

El post 6 dibujó la línea al revés. PokemonDetailView recibe un Pokémon, tres flags de estado y un callback de reintento, y los renderiza. Esa es toda la superficie, y todo el montaje son dos líneas en el contenedor del consumidor:

const { data, isLoading, isError, refetch } = useGetPokemonDetailQuery(route.params.id);
return <PokemonDetailView pokemon={data} loading={isLoading} error={isError} onRetry={refetch} />;

Pasar a datos en vivo no añadió al paquete ni una dependencia: los mismos cuatro peers que la versión estática (el contrato, React, React Native, el contexto de safe-area) y Redux en ninguna parte, porque de dónde salen los datos es asunto de la app y qué aspecto tienen es asunto de la biblioteca.

Test 2. ¿Sabe este componente de dónde salen sus datos? Un sí significa que la frontera está mal dibujada: lo que se comparte es un trozo de la app de un equipo llevando puesto el nombre de un componente.

La comprobación práctica tarda un minuto: renderízalo en una app vacía con props hardcodeadas. Si necesita que exista antes un provider, un cliente o un store, todavía no es un componente.

Las definiciones de datos viven con su dominio

Esa última release partió la pantalla en dos, la vista en un paquete y el fetch en una app, y la partición tenía que poner la definición del endpoint en alguna parte. Esa parte es la regla. getPokemonDetail aterrizó en la app list, junto a getPokemonList, porque los datos de Pokémon pertenecen al dominio Pokédex y la app list es donde vive ese dominio. A la vista le llega el resultado como props y nunca se entera de que el endpoint existe.

El contra-instinto dice que el código de datos es infraestructura y va con el resto de la infraestructura, abajo en la capa compartida. La serie parte esa afirmación en dos. baseApi, la capa HTTP con su caché y su maquinaria de tags, sí es infraestructura: no carga conocimiento de dominio, y todos los equipos ya dependen de ella como dependen de React, así que vive en el paquete de contratos y todo el mundo se acopla a ella. Un endpoint de feature es distinto. Codifica qué busca un dominio, cómo lo parsea y qué expone, y quien posea esa definición acaba poseyendo las preguntas, los bugs y las migraciones que vienen con ella. Mueve los endpoints de un dominio a una capa compartida y los dueños de la capa se convierten poco a poco en los dueños de la fontanería de datos de todos los dominios, sin el conocimiento de dominio que haría posible el trabajo.

Test 3. ¿Vive esta definición con su dominio? La maquinaria sin conocimiento de dominio se comparte; un endpoint, un parser, un esquema de los datos de un dominio viaja con el dominio.

Duplicar es más barato que una dependencia entre equipos

En el próximo post el remote de party empieza a guardar estado propio, y su primera necesidad son datos de Pokémon para un hueco tocado: datos que el dominio Pokédex ya sabe buscar. El instinto ordenado propone un paquete de datos compartido: una definición, dos consumidores, nada de deriva. La serie hace lo desordenado: el equipo de party escribe su propio fichero de endpoint. Sale en unas veinte líneas y parece un fallo de limpieza. El fichero que duplica, el del propio Pokédex, recortado a su forma:

const detailApi = baseApi.injectEndpoints({
  endpoints: build => ({
    getPokemonDetail: build.query<PokemonDetail, number>({
      async queryFn(id, _api, _extra, baseQuery) {
        const res = await baseQuery(`pokemon/${id}`);
        return res.error ? { error: res.error } : { data: parsePokemonDetail(res.data) };
      },
    }),
  }),
});

Cuenta lo que cuesta la versión ordenada. Un paquete de datos compartido entre dos equipos de feature es una dependencia entre iguales. Cada cambio le cae a los dos equipos, así que cada cambio necesita el acuerdo de los dos. Las releases se encadenan: sube el paquete, espera a que el otro equipo tome la subida, y entonces publica lo que de verdad querías publicar. Y el propio paquete necesita un dueño: uno de los dos equipos responde de sus bugs, o no responde nadie. Una definición compartida entre iguales no elimina la coordinación; convierte una deriva que puedes ver en una agenda que tienes que cumplir. La serie ya tiene fotografiado cómo queda una dependencia versionada entre equipos cuando los calendarios se separan. El rango de peer suelto del post 5 dejó que la app de un equipo emparejara un contrato viejo con una pantalla nueva, todos los compiladores quedaron en verde, y a un usuario le llegó una frase con un agujero:

La pantalla de detalle del Pokémon leyendo 'Opened from the' y nada más, porque la app list tiene una versión vieja del contrato y nunca pasó el campo que la pantalla nueva renderiza

Veinte líneas duplicadas cuestan veinte líneas. Cada copia sigue las necesidades de su equipo y sale en las fechas de su equipo, y la deriva entre ellas tiene un límite duro, porque las dos parsean el mismo formato de cable del mismo backend. Cuando la duplicación crece más allá de un fichero (cinco endpoints, diez), la respuesta cambia, y la siguiente sección dice cómo saberlo.

El test de acoplamiento

La regla de duplicar necesita un límite, porque los equipos se acoplan a código compartido todo el tiempo y hacen bien: React, el runtime de navegación, baseApi, los tipos del contrato. La línea entre esos y el paquete de datos entre iguales es el test más reutilizable de este ensayo.

Test 4. ¿Esta dependencia ya existe, o la inventa el compartir? Acóplate a aquello de lo que ya dependes; rechaza la dependencia que crea el propio compartir.

Un cliente generado de la especificación de API del backend se puede compartir tranquilamente, porque acopla a sus consumidores al backend, una dependencia que todos ellos ya tienen. Cuando la especificación cambia, cada consumidor quedó afectado en el momento en que cambió; el cliente generado solo saca el hecho a la luz en build en vez de en producción. El paquete de contratos pasa el mismo test: los params y las formas de módulo son acuerdos que ya ataban a las apps implícitamente, escritos donde todos los compiladores pueden verlos. El paquete de datos entre iguales lo suspende. Nada del dominio party dependía del calendario de releases del equipo Pokédex antes de que existiera el paquete compartido; el paquete es lo que crearía esa dependencia.

duplicar se acopla a lo que ya existeel backendPokédex: su fichero deendpointsparty: sus veinte líneasel paquete compartido inventa una dependenciacada cambiocada releaseequipo Pokédexpaquete de datos compartidoequipo party

El tirón de la shell

Todas las reglas hasta aquí empujan el código hacia fuera, hacia dominios y paquetes. Un argumento empuja en el sentido contrario, y merece la audiencia más larga del ensayo, porque suele hacerlo la persona más cuidadosa de la sala.

Los endpoints se multiplican. Varios equipos buscan datos que se solapan. Alguien propone la consolidación obvia: subir la capacidad compartida a la shell, donde el equipo de plataforma puede sostener una sola definición de todo. Una capa HTTP, una historia de autenticación, un solo juego de definiciones de datos, comportamiento consistente por construcción, deriva imposible. Nadie lo propone con cinismo. En lo puramente técnico entrega exactamente lo que promete, y una definición de verdad es más fácil de razonar que cuatro copias.

Los costes llegan de dos direcciones. El primero es organizativo, y cae antes de escribir una línea de código: escalar una capacidad a la shell convierte al equipo de plataforma en una precondición. Un equipo de feature que necesita un cambio a nivel de shell no puede empezar hasta que otro equipo agenda el trabajo, lo construye y lo publica. Su fecha de inicio se sienta ahora dentro del sprint de otros. Multiplica eso por cada equipo con una petición, y el equipo de plataforma se convierte en una cola delante de todo el programa, mientras hereda, petición a petición, conocimiento de dominio que nunca poseyó y no puede mantener al día. La palabra con la que quedarse es empezar: el equipo de feature está parado, y sigue parado hasta que publique un equipo con otras prioridades.

Test 5. ¿Puede el equipo de feature empezar sin que otro equipo publique antes? Escalar una capacidad a la shell suspende este test por diseño.

El segundo coste es específico de móvil: la shell es el binario, así que el código de shell sale al ritmo de la app store. Compilar, enviar, revisión, despliegue gradual, y una larga cola de usuarios que no actualizan nunca.

Una capacidad movida a la shell se sube al tren de la app store, y un retoque de esquema que viaja en una revisión de tienda es exactamente el coste del que esta serie adoptó la federación para escapar. La escalada lo reintroduce en silencio, capacidad a capacidad.

La capa de plataforma sí se gana un tipo de código, viaje en el binario de la shell o en un paquete propiedad de plataforma como el contrato: el tipo lento. El cliente HTTP, la autenticación, la maquinaria de baseApi, la observabilidad, los runtimes contra los que resuelve cada remote. El patrón de esa lista: capacidad que cambia despacio, no carga conocimiento de dominio, y ya tenía a todos dependiendo de ella. La lista crece rara vez, y cada añadido necesita una discusión, porque todo lo que está en ella hereda el ritmo del binario.

Inner source: el punto medio que funciona

Entre «todos los equipos esperan al equipo de plataforma» y «ninguna capacidad compartida» hay un punto medio al que las organizaciones grandes le tienen nombre: inner source. Gestionar el código compartido como un proyecto open-source que resulta ser interno: la shell, el paquete de contratos, una biblioteca de componentes, una capa de datos compartida si tu organización decide tenerla. El equipo de plataforma son los mantenedores y code owners. Los equipos de producto mandan pull requests.

La precondición bloqueante se disuelve. Un equipo de feature que necesita un cambio a nivel de shell escribe el cambio él mismo, contra las reglas de contribución del repo compartido, y espera una review en vez de un hueco en el roadmap. El trabajo del equipo de plataforma pasa de construirlo-todo a revisar-y-cuidar: sostiene el listón de calidad y coherencia y deja de ser la cola. El equipo Pokédex añadiendo un componente a la biblioteca compartida se convierte en una pull request, no en un ticket en el backlog de otro equipo. Igual que la adición de esquema del equipo party, si una capa de datos compartida es lo que eligió tu organización.

Los costes son reales. La latencia de review no desaparece; una pull request discutida puede esperar tanto como esperaba un hueco de roadmap. El equipo de plataforma acaba manteniendo código que no escribió, una carga de verdad el día que el equipo contribuyente pasa a otra cosa. Y el modelo solo funciona sobre infraestructura de contribución real:

Antes de la primera PRSobre qué funciona el inner source
Convenciones de contribución que un desconocido puede seguir, por escrito
CI que un equipo contribuyente puede ejecutar sin pedir permiso
Ficheros de ownership que enrutan cada review a la gente correcta

El inner source se paga solo cuando las contribuciones llegan lo bastante a menudo para justificar esos gastos.

«Abre una PR» contra un repo sin documentar es una cortesía, no un proceso. Sin la infraestructura de contribución, el inner source es la cola con un nombre más amable.

Los tests, y qué mueve las respuestas

El argumento entero cabe en un mapa, cada caja con lo que posee y el ritmo al que sale:

dominio party: otro equipodominio Pokédex: un equipopropiedad de plataformamonta en runtimemonta en runtimeinyecta endpoints enpublicado eninstala la vistala shellcableado del store · barra depestañas · runtimescompartidossale como el binario de la app@pokedex/contracts: losacuerdostipos · baseApi · nombres detagssemver; los majors esperanconsentimientoremote listpantallas + sus dos endpointssale cualquier tarde@pokedex/detailuna vista: props dentro,píxeles fuerasemverremote partyla cuadrícula; estado en elpróximo postsale cualquier tarderegistropor donde viajan lospaquetes

Cinco tests, en el orden en que la serie se los encontró:

Los cinco testsQuién es dueño de qué
1¿Tiene esta frontera un equipo detrás? Los dominios reciben remotes; las pantallas reciben paquetes.
2¿Sabe este componente de dónde salen sus datos? Un componente compartido renderiza lo que le dan; los datos llegan como props.
3¿Vive esta definición con su dominio? La maquinaria se comparte; los endpoints, parsers y esquemas viajan con el dominio.
4¿Esta dependencia ya existe, o la inventa el compartir? Acóplate al backend del que ya dependes; rechaza el paquete entre iguales que fabrica una dependencia de calendario entre equipos.
5¿Puede el equipo de feature empezar sin que otro equipo publique antes? Si no, la capacidad está al nivel equivocado, o lo está el proceso a su alrededor.
Los cinco están detrás de la puerta: esta app la publica más de un equipo. Si no, las carpetas los responden todos gratis.

Ninguno de estos produce una sola respuesta para todas las organizaciones; te dicen lo que cuesta cada opción en la tuya. Dos equipos que se tienen confianza y publican juntos pueden compartir un paquete de datos y apenas notar el acoplamiento. Un equipo de plataforma de una persona no puede revisar al ritmo al que contribuyen cinco equipos, y el inner source vuelve a convertirse en la cola que venía a sustituir. Una app con tres equipos y ritmo trimestral puede quedarse con todo en la shell y no notar nunca el precio. Si tu organización cortó estas líneas por otro sitio y las costuras aguantan, eso es una respuesta distinta a los mismos tests, y merece defenderse en sus propios términos.

El próximo post pone los tests a trabajar directamente. La cuadrícula del party lleva vacía desde el post 4, porque nada en la app puede alcanzarla. El equipo de party está a punto de llenarla con estado del que es dueño, y las primeras decisiones del camino son exactamente las que este ensayo acaba de defender: quién es dueño del slice, dónde va el fichero de endpoints, qué lleva el contrato. Las reglas dejan de ser prosa y empiezan a ser ficheros.

Fuentes

  • Micro Frontends — el repaso de Cam Jackson, incluido el patrón de fragmento autocontenido contra el que este ensayo argumenta en móvil
  • La ley de Conway — el paper de 1968: los sistemas reflejan las estructuras de comunicación de las organizaciones que los construyen
  • InnerSource Commons — la práctica, sus patrones, y la infraestructura de contribución de la que depende
  • Team Topologies — Skelton y Pais sobre equipos de plataforma y equipos alineados a flujo, el vocabulario detrás del debate de la shell
  • react-native-module-federation — el repo compañero cuyas decisiones defiende este ensayo
Warren de Leon
Warren de Leon

Software Engineering Manager. Recientemente lideré el equipo de Mobile Platform en Hargreaves Lansdown. Escribo sobre liderazgo técnico, React Native y cómo construir buenos equipos.

Ver perfil