Kadalasan, naiiwasan ng ordinaryong React Native app ang debateng ito, dahil ang karaniwang hugis ay isang team, isang repo, isang store. Walang anuman sa React Native ang nag-aatas niyon; ang monorepo na may ilang team at workspace package ay may parehong mga tanong sa pagmamay-ari nang walang anumang federation. Ang screen ay isang folder, ang shared component ay isang import, at ang tanong kung kanino ang isang depinisyon ng data ay may sagot na sobrang obvious kaya walang nagsasabi nito nang malakas: sa team, dahil iisa lang ang team.
Sadyang binaklas ng mga post 4 hanggang 6 ng series na ito ang kaayusang iyon: dalawang tab feature na nagbi-build at naglalabas nang mag-isa, isang screen na lumilipat sa pagitan nila bilang versioned na package, isang store na pinupuno sa runtime ng code na hindi pa nakita ng shell kailanman. Bawat hakbang ay pumilit ng isang desisyon sa pagmamay-ari: saan nakalagay ang shared screen, sino ang puwedeng humawak sa store, saan nakaupo ang isang depinisyon ng data. Bawat desisyon ay nakatanggap ng isang talata sa sandaling ginawa ito ng tutorial. Tinitipon ng post na ito ang mga desisyong iyon at ipinagtatanggol ang mga ito nang isang beses, bilang isang buo.
Nakasulat ito bilang isang set ng mga test, hindi mga hatol. Bawat seksyon ay naglalahad ng pangangatwiran at ng mga kondisyong pinaninindigan nito; baguhin ang mga kondisyon (bilang ng team, tiwala sa pagitan nila, bilis ng release, laki ng platform team) at ilang sagot ay tapat na magbabago. Dalhin mo ang mga test, itapat sa sarili mong org chart, at kontrahin ang series na ito nang may katumpakan.
Ang unang test: kailangan mo ba ang alinman dito?
Bawat problema sa sanaysay na ito ay binili, hindi minana. Ang isang single-team app na naka-organisa nang feature-first ay gumuguhit na ng mga boundary na mahalaga: isang folder bawat domain, kasama ang mga screen, hook at API call ng domain sa loob, at bawat pagtawid ay nakikita sa review bilang isang import. Walang gastos sa pagpapatakbo ang modelong iyon: walang registry, walang version number, walang pangalawang dev server. Ang isang boundary ay nare-redraw sa isang hapon sa paglilipat ng file, at ang isang emergency ay nakakatawid dito gamit ang isang import at isang TODO.
Ang federation ang nangyayari kapag ang mga folder ay naging mga team. Ang kahinaan ng folder model ay ang release train, hindi ang mga boundary: bawat folder ay lumalabas pa rin sa iisang binary, sa iisang petsa, hinihila ng pinakamabagal na feature. Kapag ang magkakahiwalay na team ay nangailangan ng magkakahiwalay na petsa ng release, ang folder boundary ay kailangang maging deployment boundary, at lahat ng tanong sa sanaysay na ito ay nagmumula sa pag-angat na iyon.
Kaya ang unang test ang pinakamura, at hinaharangan nito ang iba.
Ang gate. Mahigit ba sa isang team ang naglalabas ng app na ito? Kung hindi, tumigil ka sa mga folder. Lahat ng test sa ibaba ay nag-a-assume na oo ang sagot.
Tinitimbang nang buo ng unang post ng series na ito ang palitan, at doon din napupunta ang talatang ito: ginagawang teknikal na makinarya ng federation ang organisasyonal na friction, at ang app na walang friction ay nagbabayad para sa makinaryang hindi nito ginagamit kailanman.
Ang boundary ay isang team, hindi isang screen
Nang mangailangan ang Pokémon detail screen ng lugar na maaabot ng dalawang tab, kusang lumitaw ang federated na sagot: isang pangatlong remote, sa sarili nitong port, dineklara sa mga mapa ng dalawang consumer. Binigyan ito ng post 5 ng patas na pagdinig: gumagana ito nang mekanikal, nire-resolve ng Module Federation ang mga nested remote nang walang reklamo, at nananatiling puwedeng i-update ang screen nang hindi ginagalaw ang alinmang consumer. Hindi pa rin ang sagot, sa pangungusap na pinagmulan ng buong sanaysay na ito: ang federation boundary ay isang deployable, at hinahati ng mga team ang mga deployable ayon sa domain.
Ang pagtanggi ay tungkol sa gastos ng pagpapatakbo. May dala ang isang remote na mga gastos na hindi kilala ng folder: isang dev server na dapat patakbuhin, isang pipeline na dapat panatilihing berde, isang bersyong dapat i-publish, at sariling failure mode kapag hindi dumating ang bundle nito sa network. Ang mga gastos na iyon ay fixed bawat boundary, at ini-absorb ng isang team ang mga ito bilang overhead ng domain na aariin naman nila. Ang isang screen ay walang team sa likod. Pakawalan ang isang screen bilang sarili nitong deployable at darating ang overhead habang hindi darating kailanman ang may-ari; ibigay ang katayuang iyon nang isang beses at wala nang prinsipyadong lugar para tanggihan ang susunod na screen. Ang dulo ng daang iyon ay isang app bawat screen, at isang release process na puro koordinasyon at walang paglalabas.
Kaya ang screen ay inilabas bilang @pokedex/detail: isang package sa registry, ini-install ng dalawang tab, mino-mount ng bawat isa sa loob ng sarili nitong stack. Ang mga tab pa rin ang mga deployable; ang screen ay isang dependency. Ito ang batas ni Conway na sinasadyang gamitin: kung isasalamin din naman ng sistema ang istruktura ng komunikasyon, iguhit ang mga linya ng module kung saan naroon na ang mga linya ng team.
Pumuputol ang test sa dalawang direksyon: tinatanggihan nito ng remote ang mga screen, at binibigyan nito ng remote ang mga domain na kinipot na ng kanilang folder.
Ang shared component ay nagre-render ng ibinibigay dito
May isang kagalang-galang na pattern ang mga web micro-frontend kung saan dumarating na matalino ang isang shared fragment: kinukuha nito ang sarili nitong data, hawak ang sarili nitong mga loading state, at bumabagsak sa kahit anong host bilang tapos na feature. Totoo ang appeal. Isang team ang naghahawak ng pixels at data sa likod ng iisang seam, isang linya lang ang isinusulat ng mga consumer, at ang isang data fix ay lumalabas nang walang consumer na nagbabago ng code. Maraming web micro-frontend setup ang gumagana nang ganito nang maayos, sa bilis ng web deployment.
Ang presyo ay nasa kung ano ang kailangang dalhin ng fragment. Ang component na nagfe-fetch ay may dalang opinyon tungkol sa bawat layer sa ilalim nito: isang HTTP client, isang cache, isang retry policy, isang auth strategy. Minamana ng bawat consumer ang mga opinyong iyon nang hindi nakikita. Dalawang ganitong fragment sa iisang screen ay puwedeng magkasalungat sa lahat ng iyon. Sa mobile mas mabigat ang mana, dahil kung ano man ang dala ng fragment ay nadodoble sa bundle na kailangang i-download ng telepono.
Iginuhit ng post 6 ang linya sa kabilang direksyon. Tumatanggap ang PokemonDetailView ng isang Pokémon, tatlong state flag at isang retry callback, at nire-render ang mga ito. Iyon ang buong surface, at ang buong kaayusan ay dalawang linya sa container ng consumer:
const { data, isLoading, isError, refetch } = useGetPokemonDetailQuery(route.params.id);
return <PokemonDetailView pokemon={data} loading={isLoading} error={isError} onRetry={refetch} />;
Walang naidagdag na kahit isang dependency sa package ang pagiging live: ang parehong apat na peer ng static na bersyon (ang contract, React, React Native, ang safe-area context) at wala ang Redux kahit saan, dahil kung saan nanggagaling ang data ay trabaho ng app at kung ano ang itsura nito ay trabaho ng library.
Sulit huminto sa isang oo. Puwede itong maging tamang desisyon, para sa widget na inaalagaan ng platform team mula dulo hanggang dulo at gusto ng lahat ng consumer na magkapareho. Mali ang pagkakaguhit ng boundary kapag ang pagkuha ng data ay pahabol lang sa ginagawa ng component, dahil ang ibinabahagi noon ay isang piraso ng app ng isang team na nakasuot ng pangalan ng component.
Isang minuto lang ang praktikal na pagsusuri: i-render ito sa isang hubad na app gamit ang mga hardcoded na prop. Ang bawat provider, client o store na hinihingi nito bago mag-render ay piraso ng runtime ng isang app na ipinapatayong muli ngayon ng package sa bawat consumer. Makatuwirang hingin ang isang theme context; ang query client na naka-wire sa mga endpoint ng iisang team ay isang app na nagsusuot ng pangalan ng component.
Ang mga depinisyon ng data ay kasama ng kanilang domain
Hinati ng huling release na iyon ang screen sa dalawa, ang view sa isang package at ang fetch sa isang app, at kinailangan ng hati na ilagay ang depinisyon ng endpoint sa kung saan. Ang kung saang iyon ang panuntunan. Lumapag ang getPokemonDetail sa list app, katabi ng getPokemonList, dahil ang Pokémon data ay pag-aari ng Pokédex domain at nasa list app ang domain na iyon. Ang view ay pinapakain ng resulta bilang props at hindi nalalaman kailanman na nandiyan ang endpoint.
Sabi ng kabaligtarang instinct, ang data code ay infrastructure at kasama ng iba pang infrastructure, sa baba sa shared layer. Hinahati ng series ang claim na iyon sa dalawa. Ang baseApi, ang HTTP layer kasama ang cache at tag machinery nito, ay talagang infrastructure: wala itong dalang kaalaman sa domain, at umaasa na rito ang bawat team gaya ng pag-asa nila sa React, kaya nasa contract package ito at naka-couple dito ang lahat. Iba ang isang feature endpoint. Nakapaloob dito kung ano ang kinukuha ng isang domain, paano nito pina-parse, at ano ang inilalantad nito, at kung sino man ang may-ari ng depinisyong iyon ay nagiging may-ari ng mga tanong, mga bug at mga migration na kasama nito. Ilipat ang mga endpoint ng isang domain sa shared layer at ang mga may-ari ng layer ay dahan-dahang nagiging may-ari ng data plumbing ng bawat domain, nang wala ang kaalaman sa domain na magpapaposible sa trabaho.
Ang endpoint (kung ano ang kinukuha ng domain at kailan) ay sumasama sa domain. Ang makinaryang walang kaalaman sa domain ay ibinabahagi. Nasa gitna ang mga model at parser, at sumusunod sa backend ang hatian: ang schema para sa wire format na inaasahan na ng lahat ng consumer ay nasa contract package (ang lohika ng test 4), samantalang ang schema na iisang domain lang ang nagbabasa ay nananatili sa domain na iyon, at kaya nga pinaparse ng series na ito ang REST payload ng PokéAPI sa contract at pinananatili ng post 10 ang pagparse ng GraphQL nito sa loob ng list app.
Mas mura ang pagdodoble kaysa sa dependency sa pagitan ng mga team
Sa susunod na post, magsisimulang maghawak ang party remote ng sarili nitong state, at ang una nitong pangangailangan ay Pokémon data para sa isang tinapik na slot: data na alam nang kunin ng Pokédex domain. Ang maayos na instinct ay nagmumungkahi ng shared data package: isang depinisyon, dalawang consumer, walang drift. Gagawin ng series ang magulong bagay: susulat ang party team ng sarili nitong endpoint file. Aabot ito sa mga dalawampung linya at mukhang kabiguan sa paglilinis. Ang file na dinodoble nito, ang sa mismong Pokédex, pinutol sa hugis nito:
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) };
},
}),
}),
});
Bilangin kung magkano ang maayos na bersyon. Ang isang data package na pinaghahatian ng dalawang feature team ay isang dependency sa pagitan ng magkapantay. Tahimik na lumalapag ang isang compatible na release, dahil nasa caret ang mga consumer at kinukuha nila ito sa susunod nilang pag-install. Pero hindi lahat ng pagbabagong sulit gawin sa isang shared na depinisyon ay compatible, at sa araw na may pumasok na breaking change, kailangang mailipat ang bawat consumer bago maalis ang lumang bersyon. Nagkakadena ang mga release: i-bump ang package, hintayin ang kabilang team na kunin ang bump, saka ilabas ang talagang gusto mong ilabas. At ang package mismo ay nangangailangan ng may-ari: isa sa dalawang team ang sasagot sa mga bug nito, o walang sasagot. Ang isang shared na depinisyon sa pagitan ng magkapantay ay hindi nag-aalis ng koordinasyon; ginagawa nitong iskedyul na dapat mong tuparin ang drift na nakikita mo.
Hinayaan ng maluwag na peer range ng post 5 na ipares ng app ng isang team ang lumang contract sa bagong screen, nanatiling berde ang bawat compiler, at nakatanggap ang isang user ng pangungusap na may butas:
Ang dalawampung nadobleng linya ay nagkakahalaga ng dalawampung linya. Sumusunod ang bawat kopya sa pangangailangan ng sarili nitong team at lumalabas sa mga petsa ng sarili nitong team, at nananatiling nakikita ang drift sa pagitan nila, dahil parehong pina-parse ng dalawa ang parehong wire format mula sa parehong backend: puwedeng maghiwalay ang mga kopya sa kung ano ang itinatago nila at kung paano nila ito hinuhubog, pero lumilitaw ang paghihiwalay laban sa shared na payload sa halip na magtago sa likod ng dalawang magkaibang payload. Kapag lumaki ang pagdodoble lampas sa isang file (limang endpoint, sampu), nagbabago ang sagot, at sinasabi ng Ang coupling test kung paano ito malalaman.
Ang coupling test
Kailangan ng panuntunan ng pagdodoble ng hangganan, dahil palaging naka-couple ang mga team sa shared code at tama sila: React, ang navigation runtime, baseApi, ang mga type ng contract. Ang linya sa pagitan ng mga iyon at ng peer data package ang pinaka-reusable na test sa sanaysay na ito.
Kumabit sa inaasahan mo na; tanggihan ang dependency na nililikha mismo ng pagbabahagi.
Ang isang client na ginenerate mula sa API specification ng backend ay ligtas ibahagi, dahil ikino-couple nito ang mga consumer nito sa backend, isang dependency na mayroon na silang lahat. Kapag hindi compatible ang pagbabago sa specification, apektado na ang bawat consumer sa sandaling nagbago ito; inilalabas lang ng generated na client ang katotohanan sa build time sa halip na sa production, at ang additive na pagbabago ay matiyagang naghihintay na pansinin ito ng bawat consumer. Pumapasa ang contract package sa parehong test: ang mga param at hugis ng module ay mga kasunduang nakatali na sa mga app nang implicit, isinulat kung saan nakikita ng bawat compiler. Bumabagsak dito ang peer data package. Walang anuman sa party domain ang umasa sa iskedyul ng release ng Pokédex team bago nabuo ang shared package; ang package mismo ang lilikha ng dependency na iyon.
Ang hila ng shell
Bawat panuntunan hanggang dito ay nagtutulak ng code palabas, patungo sa mga domain at package. May isang argumentong nagtutulak sa kabilang direksyon, at karapat-dapat ito sa pinakamahabang pagdinig sa sanaysay, dahil kadalasang ginagawa ito ng pinakamaingat na inhinyero sa silid.
Dumarami ang mga endpoint. Ilang team ang kumukuha ng nagsasapawang data. May nagmumungkahi ng obvious na konsolidasyon: iakyat ang shared na kakayahan sa shell, kung saan kayang hawakan ng platform team ang iisang depinisyon ng lahat. Isang HTTP layer, isang auth strategy, isang set ng mga depinisyon ng data, consistent na behaviour dahil sa konstruksyon, imposibleng drift. Walang nagmumungkahi nito nang may masamang hangarin. Sa purong teknikal, naibibigay nito nang eksakto ang ipinapangako, at ang iisang depinisyon ay talagang mas madaling pag-isipan kaysa apat na kopya.
Dumarating ang mga gastos mula sa dalawang direksyon. Ang una ay organisasyonal, at bumabagsak bago pa maisulat ang anumang code: ang pag-escalate ng kakayahan sa shell ay ginagawang precondition ang platform team. Ang feature team na nangangailangan ng pagbabago sa antas ng shell ay hindi makakapagsimula hangga’t hindi ini-schedule, binubuo at inilalabas ng ibang team ang trabaho. Ang petsa ng simula nito ay nakaupo na ngayon sa loob ng sprint ng iba. I-multiply iyan sa bawat team na may kahilingan, at ang platform team ay nagiging pila sa harap ng buong programa, habang nagmamana, kahilingan bawat kahilingan, ng kaalaman sa domain na hindi nito inari kailanman at hindi nito kayang panatilihing napapanahon. Ang salitang dapat pagnilayan ay magsimula: ang feature team ay nakatigil, at nananatiling nakatigil hangga’t hindi naglalabas ang isang team na may ibang mga prayoridad.
Ang pag-escalate ng kakayahan sa shell ay bumabagsak dito ayon sa disenyo.
Ang pangalawang gastos ay partikular sa mobile: ang shell ang binary, kaya ang shell code ay lumalabas sa bilis ng app store. Build, submit, review, staged rollout, tapos ang mahabang buntot ng mga user na hindi nag-a-update kailanman.
Ang kakayahang inilipat sa shell ay sumasakay sa tren ng app store, at ang isang maliit na pagbabago sa schema na sumasakay sa store review ang mismong gastos na tinakasan ng series na ito nang i-adopt ang federation. Tahimik itong ibinabalik ng escalation, isa-isang kakayahan.
May isang uri ng code na talagang napapasakamay ng platform layer, sumakay man ito sa binary ng shell o sa isang package na pag-aari ng platform gaya ng contract: ang mabagal na uri. Ang HTTP client, ang auth, ang makinarya ng baseApi, ang observability, ang mga runtime na nireresolba ng bawat remote. Ang pattern sa listahang iyon: kakayahang mabagal magbago, walang dalang kaalaman sa domain, at inaasahan na ng lahat. Bihirang lumaki ang listahan, at bawat dagdag ay nangangailangan ng argumento, dahil lahat ng nasa listahan ay nagmamana ng bilis ng binary.
Inner source: ang gumaganang gitna
Sa pagitan ng “lahat ng team ay naghihintay sa platform team” at “walang shared na kakayahan” ay may gitnang may pangalan sa malalaking organisasyon: inner source. Patakbuhin ang shared code na parang open-source project na nagkataong internal: ang shell, ang contract package, ang isang component library, ang isang shared data layer kung magpasya ang organisasyon mong magkaroon. Ang platform team ang mga maintainer at code owner. Ang mga product team ay nagpapadala ng mga pull request.
Natutunaw ang humaharang na precondition. Ang feature team na nangangailangan ng pagbabago sa antas ng shell ay mismong nagsusulat ng pagbabago, laban sa mga panuntunan ng kontribusyon ng shared repo, at naghihintay ng review sa halip na ng puwesto sa roadmap. Ang trabaho ng platform team ay lumilipat mula sa gawin-lahat patungo sa suriin-at-alagaan: hawak nito ang pamantayan ng kalidad at pagkakaugnay at tumitigil itong maging pila. Ang Pokédex team na nagdadagdag ng component sa shared library ay nagiging isang pull request, hindi isang ticket sa backlog ng ibang team. Gayundin ang dagdag na schema ng party team, kung shared data layer ang napili ng organisasyon mo.
Totoo ang mga gastos. Hindi nawawala ang latency ng review; ang isang pinagtatalunang pull request ay puwedeng maghintay nang kasintagal ng puwesto sa roadmap noon. Ang platform team ay napupunta sa pagmamantini ng code na hindi nito isinulat, isang tunay na pasanin sa araw na lumipat ang nag-ambag na team. At gumagana lang ang modelo sa ibabaw ng tunay na imprastruktura ng kontribusyon:
Nagbabayad ang inner source para sa sarili nito kapag sapat na kadalas dumating ang mga kontribusyon para bigyang-katwiran ang overhead na iyon.
Ang "mag-open ka na lang ng PR" laban sa isang repo na walang dokumentasyon ay isang kagandahang-asal, hindi isang proseso. Kung wala ang imprastruktura ng kontribusyon, ang inner source ay ang pila na may mas palakaibigang pangalan.
Ang mga test, at kung ano ang nagpapabago sa mga sagot
Kasya sa isang mapa ang buong argumento, bawat kahon ay hawak ang pag-aari nito at ang bilis ng paglabas nito:
Limang test, sa pagkakasunud-sunod ng pagkakatagpo ng series sa kanila:
Walang isa man dito ang nagbibigay ng iisang sagot para sa bawat organisasyon; sinasabi nila kung magkano ang halaga ng bawat opsyon sa iyo. Dalawang team na may tiwala sa isa’t isa at sabay naglalabas ay puwedeng maghatian ng data package at halos hindi maramdaman ang coupling. Ang app na may tatlong team at quarterly na bilis ay puwedeng panatilihin ang lahat sa shell at hindi mapansin kailanman ang presyo. Kung iba ang pagkakaputol ng organisasyon mo sa mga linyang ito at kumakapit ang mga seam, iyon ay ibang sagot sa parehong mga test, at karapat-dapat ipagtanggol sa sarili nitong mga termino.
Diretsong pinagtatrabaho ng susunod na post ang mga test. Walang laman ang party grid mula pa post 4, dahil walang anuman sa app ang nakakaabot dito. Malapit na itong punuin ng party team ng state na pag-aari nito, at ang mga unang desisyon sa daan ay eksaktong ang mga ipinagtanggol lang ng sanaysay na ito: kanino ang slice, saan pupunta ang endpoint file, ano ang dala ng contract. Ang mga panuntunan ay titigil na maging prosa at magsisimulang maging mga file.
Mga Sanggunian
- Micro Frontends — ang pagsusuri ni Cam Jackson, kasama ang self-contained fragment pattern na kinokontra ng sanaysay na ito para sa mobile
- Batas ni Conway — ang papel ng 1968: isinasalamin ng mga sistema ang mga istruktura ng komunikasyon ng mga organisasyong bumubuo sa kanila
- InnerSource Commons — ang praktis, ang mga pattern nito, at ang imprastruktura ng kontribusyon na inaasahan nito
- Team Topologies — sina Skelton at Pais tungkol sa mga platform team at stream-aligned team, ang bokabularyo sa likod ng debate ng shell
- react-native-module-federation — ang kasamang repo na ang mga desisyon ay ipinagtatanggol ng sanaysay na ito