Una app React Native normal no té mai aquesta discussió. Un equip, un repo, un store. Una pantalla és una carpeta, un component compartit és un import, i la pregunta de qui és amo d’una definició de dades té una resposta tan òbvia que ningú no la diu en veu alta: l’equip, perquè només n’hi ha un.
Els posts 4 a 6 d’aquesta sèrie van desmuntar aquest arranjament a propòsit: dues features de pestanya que es construeixen i es publiquen pel seu compte, una pantalla que viatja entre elles com a paquet versionat, un store que omple en runtime codi que la shell no ha vist mai. Cada pas va obligar a una decisió de propietat: on viu la pantalla compartida, qui pot tocar el store, on seu una definició de dades. Cada decisió va rebre un paràgraf en el moment en què el tutorial la va prendre. Aquest post les reuneix i les defensa d’una vegada, com un tot.
Està escrit com un conjunt de tests, no de veredictes. Cada secció exposa el raonament i les condicions sota les quals se sosté; canvia les condicions (nombre d’equips, confiança entre ells, ritme de releases, la mida de l’equip de plataforma) i algunes respostes canvien amb tota honestedat. Emporta’t els tests, posa’ls davant del teu propi organigrama, i discrepa d’aquesta sèrie amb precisió.
El primer test: necessites res de tot això?
Cada problema d’aquest assaig és triat, no heretat. Una app d’un sol equip organitzada feature-first ja dibuixa les fronteres que importen: una carpeta per domini, amb les pantalles, hooks i crides a API del domini a dins, i cada encreuament visible en review com un import. Aquest model no costa res de mantenir: ni registre, ni números de versió, ni un segon dev server. Una frontera es redibuixa en una tarda movent fitxers, i una emergència la creua amb un import i un TODO.
La federació és el que passa quan les carpetes es converteixen en equips. La debilitat del model de carpetes és el tren de releases, no les fronteres: cada carpeta segueix sortint en un sol binari, en una sola data, retinguda per la feature més lenta. Quan equips separats necessiten dates de release separades, la frontera de carpeta ha de convertir-se en frontera de desplegament, i totes les preguntes d’aquest assaig surten d’aquest canvi de rang.
Així que el primer test és el més barat, i bloqueja la resta.
La porta. Publica aquesta app més d'un equip? Si no, queda't a les carpetes. Tots els tests de sota assumeixen que la resposta és sí.
El primer post d’aquesta sèrie pesa la contrapartida sencera i aterra on aquest paràgraf: la federació converteix fricció organitzativa en maquinària tècnica, i una app sense la fricció paga una maquinària que no fa servir mai.
Una frontera és un equip, no una pantalla
Quan la pantalla de detall del Pokémon va necessitar un lloc al qual arribessin les dues pestanyes, la resposta federada es proposava sola: un tercer remote, al seu propi port, declarat als mapes dels dos consumidors. El post 5 li va donar una audiència justa: funciona mecànicament, Module Federation resol remotes aniuats sense queixar-se, i la pantalla queda actualitzable sense tocar cap consumidor. La resposta va seguir sent no, en la frase de la qual creix aquest assaig sencer: una frontera de federació és una unitat de desplegament, i els equips tallen les unitats de desplegament per dominis.
El rebuig va de costos de funcionament. Un remote carrega costos que una carpeta no coneix: un dev server que mantenir en marxa, un pipeline que mantenir en verd, una versió que publicar, i un mode de fallada propi quan el seu bundle no arriba per la xarxa. Aquests costos són fixos per frontera, i un equip els absorbeix com a despeses generals d’un domini que anava a posseir de totes maneres. Una pantalla no té equip al darrere. Deixa anar una pantalla com a unitat de desplegament pròpia i les despeses apareixen mentre l’amo no apareix mai; concedeix aquest estatus una vegada i no queda cap lloc amb principis on negar-l’hi a la pantalla següent. El final d’aquest camí és una app per pantalla, i un procés de release que és tot coordinació i gens de publicar.
Així que la pantalla es va publicar com a @pokedex/detail: un paquet en un registre, instal·lat per les dues pestanyes, muntat per cadascuna dins del seu propi stack. Les pestanyes segueixen sent les unitats de desplegament; la pantalla és una dependència. Això és la llei de Conway usada a propòsit: si el sistema reflectirà l’estructura de comunicació de totes maneres, dibuixa les línies de mòduls on ja hi ha les línies d’equips.
Test 1. Té aquesta frontera un equip al darrere? El test talla en les dues direccions: els nega remotes a les pantalles, i els concedeix als dominis que han quedat petits a la seva carpeta.
Un component compartit renderitza el que li donen
Els micro-frontends web tenen un patró respectable en què un fragment compartit arriba llest: busca les seves pròpies dades, gestiona els seus propis estats de càrrega, i cau en qualsevol host com una feature acabada. L’atractiu és real. Un equip guarda píxels i dades darrere d’una sola frontera, els consumidors escriuen una sola línia, i un arranjament de dades surt sense que cap consumidor canviï codi. Molts muntatges de micro-frontends web funcionen així amb coherència, al ritme de desplegament de la web.
El preu és en el que el fragment ha de carregar. Un component que fa fetch carrega amb una opinió sobre cada capa que té a sota: un client HTTP, un cache, una política de reintents, una estratègia d’autenticació. Cada consumidor hereta aquestes opinions sense veure-les. Dos fragments així en una mateixa pantalla poden discrepar de tot plegat. En mòbil l’herència pesa més, perquè el que el fragment carrega es duplica en un bundle que un telèfon ha de descarregar.
El post 6 va dibuixar la línia a l’inrevés. PokemonDetailView rep un Pokémon, tres flags d’estat i un callback de reintent, i els renderitza. Aquesta és tota la superfície, i tot el muntatge són dues línies al contenidor del consumidor:
const { data, isLoading, isError, refetch } = useGetPokemonDetailQuery(route.params.id);
return <PokemonDetailView pokemon={data} loading={isLoading} error={isError} onRetry={refetch} />;
Passar a dades en directe no va afegir al paquet ni una dependència: els mateixos quatre peers que la versió estàtica (el contracte, React, React Native, el context de safe-area) i Redux enlloc, perquè d’on surten les dades és cosa de l’app i quin aspecte tenen és cosa de la biblioteca.
Test 2. Sap aquest component d’on surten les seves dades? Un sí vol dir que la frontera està mal dibuixada: el que es comparteix és un tros de l’app d’un equip portant posat el nom d’un component.
La comprovació pràctica triga un minut: renderitza’l en una app buida amb props hardcodejades. Si necessita que existeixi abans un provider, un client o un store, encara no és un component.
Les definicions de dades viuen amb el seu domini
Aquella última release va partir la pantalla en dues, la vista en un paquet i el fetch en una app, i la partició havia de posar la definició de l’endpoint en algun lloc. Aquest lloc és la regla. getPokemonDetail va aterrar a l’app list, al costat de getPokemonList, perquè les dades de Pokémon pertanyen al domini Pokédex i l’app list és on viu aquest domini. A la vista li arriba el resultat com a props i no s’assabenta mai que l’endpoint existeix.
El contra-instint diu que el codi de dades és infraestructura i va amb la resta de la infraestructura, a baix a la capa compartida. La sèrie parteix aquesta afirmació en dues. baseApi, la capa HTTP amb el seu cache i la seva maquinària de tags, sí que és infraestructura: no carrega coneixement de domini, i tots els equips ja en depenen com depenen de React, així que viu al paquet de contractes i tothom s’hi acobla. Un endpoint de feature és diferent. Codifica què busca un domini, com ho parseja i què exposa, i qui posseeixi aquesta definició acaba posseint les preguntes, els bugs i les migracions que venen amb ella. Mou els endpoints d’un domini a una capa compartida i els amos de la capa es converteixen a poc a poc en els amos de la fontaneria de dades de tots els dominis, sense el coneixement de domini que faria possible la feina.
Test 3. Viu aquesta definició amb el seu domini? La maquinària sense coneixement de domini es comparteix; un endpoint, un parser, un esquema de les dades d’un domini viatja amb el domini.
Duplicar és més barat que una dependència entre equips
Al proper post el remote de party comença a guardar estat propi, i la seva primera necessitat són dades de Pokémon per a un buit tocat: dades que el domini Pokédex ja sap buscar. L’instint endreçat proposa un paquet de dades compartit: una definició, dos consumidors, gens de deriva. La sèrie fa la cosa desendreçada: l’equip de party escriu el seu propi fitxer d’endpoint. Surt en unes vint línies i sembla una fallada de neteja. El fitxer que duplica, el del mateix Pokédex, retallat a la seva 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) };
},
}),
}),
});
Suma el que costa la versió endreçada. Un paquet de dades compartit entre dos equips de feature és una dependència entre iguals. Cada canvi cau als dos equips, així que cada canvi necessita l’acord dels dos. Les releases s’encadenen: puja el paquet, espera que l’altre equip prengui la pujada, i llavors publica el que de debò volies publicar. I el mateix paquet necessita un amo: un dels dos equips respon dels seus bugs, o no en respon ningú. Una definició compartida entre iguals no elimina la coordinació; converteix una deriva que pots veure en una agenda que has de complir. La sèrie ja té fotografiat com queda una dependència versionada entre equips quan els calendaris se separen. El rang peer lax del post 5 va deixar que l’app d’un equip aparellés un contracte vell amb una pantalla nova, tots els compiladors van quedar en verd, i a un usuari li va arribar una frase amb un forat:
Vint línies duplicades costen vint línies. Cada còpia segueix les necessitats del seu equip i surt en les dates del seu equip, i la deriva entre elles té un límit dur, perquè les dues parsegen el mateix format de cable del mateix backend. Quan la duplicació creix més enllà d’un fitxer (cinc endpoints, deu), la resposta canvia, i la secció següent diu com saber-ho.
El test d’acoblament
La regla de duplicar necessita un límit, perquè els equips s’acoblen a codi compartit tota l’estona i fan bé: React, el runtime de navegació, baseApi, els tipus del contracte. La línia entre aquests i el paquet de dades entre iguals és el test més reutilitzable d’aquest assaig.
Test 4. Aquesta dependència ja existeix, o la inventa el compartir? Acobla’t a allò de què ja depens; rebutja la dependència que crea el mateix compartir.
Un client generat de l’especificació d’API del backend es pot compartir tranquil·lament, perquè acobla els seus consumidors al backend, una dependència que tots ells ja tenen. Quan l’especificació canvia, cada consumidor va quedar afectat en el moment en què va canviar; el client generat només treu el fet a la llum en build en lloc de en producció. El paquet de contractes passa el mateix test: els params i les formes de mòdul són acords que ja lligaven les apps implícitament, escrits on tots els compiladors els poden veure. El paquet de dades entre iguals el suspèn. Res del domini party no depenia del calendari de releases de l’equip Pokédex abans que existís el paquet compartit; el paquet és el que crearia aquesta dependència.
L’atracció de la shell
Totes les regles fins aquí empenyen el codi cap enfora, cap a dominis i paquets. Un argument empeny en el sentit contrari, i mereix l’audiència més llarga de l’assaig, perquè sol fer-lo la persona més acurada de la sala.
Els endpoints es multipliquen. Diversos equips busquen dades que se solapen. Algú proposa la consolidació òbvia: pujar la capacitat compartida a la shell, on l’equip de plataforma pot sostenir una sola definició de tot. Una capa HTTP, una estratègia d’autenticació, un sol joc de definicions de dades, comportament consistent per construcció, deriva impossible. Ningú no ho proposa amb cinisme. En el purament tècnic entrega exactament el que promet, i una definició de debò és més fàcil de raonar que quatre còpies.
Els costos arriben de dues direccions. El primer és organitzatiu, i cau abans d’escriure una línia de codi: escalar una capacitat a la shell converteix l’equip de plataforma en una precondició. Un equip de feature que necessita un canvi a nivell de shell no pot començar fins que un altre equip agenda la feina, la construeix i la publica. La seva data d’inici depèn ara de l’sprint d’uns altres. Multiplica això per cada equip amb una petició, i l’equip de plataforma es converteix en una cua davant de tot el programa, mentre hereta, petició a petició, coneixement de domini que mai no va posseir i no pot mantenir al dia. La paraula amb què quedar-se és començar: l’equip de feature està aturat, i segueix aturat fins que publiqui un equip amb altres prioritats.
Test 5. Pot l’equip de feature començar sense que un altre equip publiqui abans? Escalar una capacitat a la shell suspèn aquest test per disseny.
El segon cost és específic de mòbil: la shell és el binari, així que el codi de shell surt al ritme de l’app store. Compilar, enviar, revisió, desplegament gradual, i una llarga cua d’usuaris que no actualitzen mai.
Una capacitat moguda a la shell puja al tren de l'app store, i un retoc d'esquema que viatja en una revisió de botiga és exactament el cost del qual aquesta sèrie va adoptar la federació per escapar. L'escalada el reintrodueix en silenci, capacitat a capacitat.
La capa de plataforma sí que es guanya un tipus de codi, viatgi al binari de la shell o en un paquet propietat de plataforma com el contracte: el tipus lent. El client HTTP, l’autenticació, la maquinària de baseApi, l’observabilitat, els runtimes contra els quals resol cada remote. El patró d’aquesta llista: capacitat que canvia a poc a poc, no carrega coneixement de domini, i ja tenia tothom depenent d’ella. La llista creix rarament, i cada afegit necessita una discussió, perquè tot el que hi és hereta el ritme del binari.
Inner source: el punt mitjà que funciona
Entre «tots els equips esperen l’equip de plataforma» i «cap capacitat compartida» hi ha un punt mitjà que les organitzacions grans ja han batejat: inner source. Gestionar el codi compartit com un projecte open-source que resulta ser intern: la shell, el paquet de contractes, una biblioteca de components, una capa de dades compartida si la teva organització decideix tenir-ne una. L’equip de plataforma són els mantenidors i code owners. Els equips de producte envien pull requests.
La precondició bloquejant es dissol. Un equip de feature que necessita un canvi a nivell de shell escriu el canvi ell mateix, contra les regles de contribució del repo compartit, i espera una review en lloc d’un espai al roadmap. La feina de l’equip de plataforma passa de construir-ho-tot a revisar-i-cuidar: sosté el llistó de qualitat i coherència i deixa de ser la cua. Que l’equip Pokédex afegeixi un component a la biblioteca compartida es converteix en una pull request, no en un tiquet al backlog d’un altre equip. Igual que l’afegit d’esquema de l’equip party, si una capa de dades compartida és el que va triar la teva organització.
Els costos són reals. La latència de review no desapareix; una pull request discutida pot esperar tant com esperava un espai de roadmap. L’equip de plataforma acaba mantenint codi que no va escriure, una càrrega de debò el dia que l’equip contribuent passa a una altra cosa. I el model només funciona sobre infraestructura de contribució real:
L’inner source s’amortitza quan les contribucions arriben prou sovint per justificar aquestes despeses.
«Obre una PR» contra un repo sense documentar és una cortesia, no un procés. Sense la infraestructura de contribució, l'inner source és la cua amb un nom més amable.
Els tests, i què mou les respostes
L’argument sencer cap en un mapa, cada caixa amb el que posseeix i el ritme al qual surt:
Cinc tests, en l’ordre en què la sèrie se’ls va trobar:
Cap d’aquests no produeix una sola resposta per a totes les organitzacions; et diuen el que costa cada opció a la teva. Dos equips que es tenen confiança i publiquen junts poden compartir un paquet de dades i gairebé no notar l’acoblament. Un equip de plataforma d’una persona no pot revisar al ritme al qual contribueixen cinc equips, i l’inner source torna a convertir-se en la cua que venia a substituir. Una app amb tres equips i ritme trimestral pot quedar-se amb tot a la shell i no notar mai el preu. Si la teva organització va tallar aquestes línies per un altre lloc i les fronteres aguanten, això és una resposta diferent als mateixos tests, i mereix defensar-se en els seus propis termes.
El proper post posa els tests a treballar directament. La graella de l’equip és buida des del post 4, perquè res a l’app no hi pot arribar. L’equip de party és a punt d’omplir-la amb estat del qual és amo, i les primeres decisions del camí són exactament les que aquest assaig acaba de defensar: qui és amo de l’slice, on va el fitxer d’endpoints, què porta el contracte. Les regles deixen de ser prosa i comencen a ser fitxers.
Fonts
- Micro Frontends — el repàs de Cam Jackson, inclòs el patró de fragment autocontingut contra el qual aquest assaig argumenta en mòbil
- La llei de Conway — el paper de 1968: els sistemes reflecteixen les estructures de comunicació de les organitzacions que els construeixen
- InnerSource Commons — la pràctica, els seus patrons, i la infraestructura de contribució de la qual depèn
- Team Topologies — Skelton i Pais sobre equips de plataforma i equips alineats a flux, el vocabulari darrere del debat de la shell
- react-native-module-federation — el repo company les decisions del qual defensa aquest assaig