A federated app drawn as territories owned by different teams, joined by thin shared rails and one small central shell

Who owns what: boundaries, teams and coupling in a federated React Native app

A plain React Native app never has this argument. One team, one repo, one store. A screen is a folder, a shared component is an import, and the question of who owns a data definition has an answer too obvious to say out loud: the team does, because there is only one.

Posts 4 to 6 of this series took that arrangement apart on purpose: two tab features that build and ship on their own, a screen that travels between them as a versioned package, one store filled at runtime by code the shell has never seen. Every step forced an ownership call: where the shared screen lives, who may touch the store, where a data definition sits. Each call got a paragraph at the moment the tutorial made it. This post gathers those calls and argues them once, as a whole.

It is written as a set of tests, not a set of verdicts. Each section states the reasoning and the conditions under which it holds; change the conditions (team count, trust between teams, release cadence, the size of the platform team) and some of the answers flip honestly. Take the tests away, hold them against your own org chart, and disagree with this series precisely.

The first test: do you need any of this?

Every problem in this essay is bought, not inherited. A single-team app organised feature-first already draws the boundaries that matter: a folder per domain, with the domain’s screens, hooks and API calls inside it, and every crossing visible in review as an import. That model costs nothing to run: no registry, no version numbers, no second dev server. A boundary can be redrawn in an afternoon with a file move, and an emergency can cross one with an import and a TODO.

Federation is what happens when the folders become teams. The folder model’s weakness is the release train, not the boundaries: every folder still ships in one binary, on one date, gated by the slowest feature on it. When separate teams need separate release dates, the folder boundary has to become a deployment boundary, and every question in this essay follows from that promotion.

So the first test is the cheapest one, and it gates the rest.

The gate. Does more than one team ship this app? If not, stop at folders. Every test below assumes the answer is yes.

The first post of this series weighs the trade in full and lands where this paragraph does: federation converts organisational friction into technical machinery, and an app without the friction pays for machinery it never uses.

A boundary is a team, not a screen

When the Pokémon detail screen needed a home both tabs could reach, the federated answer suggested itself: a third remote, on its own port, declared in both consumers’ maps. Post 5 gave that option a fair hearing: it works mechanically, Module Federation resolves nested remotes without complaint, and the screen stays updatable without touching either consumer. The answer was still no, in the sentence this whole essay grows from: a federation boundary is a deployable, and teams cut deployables along domains.

The refusal is about running costs. A remote carries costs a folder never does: a dev server to run, a pipeline to keep green, a version to publish, and a failure mode of its own when its bundle fails to arrive over the network. Those costs are fixed per boundary, and a team absorbs them as overhead on a domain it was going to own anyway. A screen has no team behind it. Cut a screen loose as its own deployable and the overhead turns up while the owner never does; grant that status once and there is no principled place to refuse the next screen. The end of that road is an app per screen, and a release process that is all coordination and no shipping.

cut by domain: one team per boxPokédex domainall its screensparty domainall its screenscut by screen: no ownerslistdetailsettings

So the screen shipped as @pokedex/detail instead: a package on a registry, installed by both tabs, mounted by each inside its own stack. The tabs stay the deployables; the screen is a dependency. This is Conway’s law used deliberately: if the system is going to mirror the communication structure anyway, draw the module lines where the team lines already are.

Test 1. Does this boundary have a team behind it? The test cuts in both directions: it refuses remotes to screens, and it grants them to domains that have outgrown their folder.

A shared component renders what it is given

Web micro-frontends have a respectable pattern in which a shared fragment arrives smart: it fetches its own data, owns its own loading states, and drops into any host as a finished feature. The appeal is real. One team keeps pixels and data behind one seam, consumers write a single line, and a data fix rolls out with no consumer changing code. Plenty of web micro-frontend setups run this way coherently, at web deployment cadence.

The price is in what the fragment has to carry. A component that fetches carries an opinion about every layer beneath it: an HTTP client, a cache, a retry policy, an auth story. Every consumer inherits those opinions without seeing them. Two such fragments on one screen can disagree about all of it. On mobile the inheritance weighs more, because whatever the fragment carries is duplicated into a bundle a phone has to download.

Post 6 drew the line the other way. PokemonDetailView takes a Pokémon, three state flags and a retry callback, and renders them. That is the whole surface, and the whole arrangement is two lines in the consumer’s container:

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

Going live added no dependencies to the package at all: the same four peers as the static version (the contract, React, React Native, the safe-area context) and Redux nowhere among them, because where the data comes from is the app’s business and what it looks like is the library’s.

Test 2. Does this component know where its data comes from? A yes means the boundary is misdrawn: what is being shared is a piece of one team’s app wearing a component’s name.

The practical check takes a minute: render it in a bare app with hardcoded props. If it needs a provider, a client or a store to exist first, it is not a component yet.

Data definitions live with their domain

That last release split the screen in two, the view in a package and the fetch in an app, and the split had to put the endpoint definition somewhere. The somewhere is the rule. getPokemonDetail landed in the list app, next to getPokemonList, because Pokémon data belongs to the Pokédex domain and the list app is where that domain lives. The view is fed the result as props and never learns the endpoint exists.

The counter-instinct says data code is infrastructure and belongs with the rest of the infrastructure, down in the shared layer. The series splits that claim in two. baseApi, the HTTP layer with its cache and tag machinery, genuinely is infrastructure: it carries no domain knowledge, and every team already depends on it the way they depend on React, so it lives in the contract package and everyone couples to it. A feature endpoint is different. It encodes what one domain fetches, how it parses, and what it exposes, and whoever owns that definition ends up owning the questions, the bugs and the migrations that come with it. Move a domain’s endpoints into a shared layer and the layer’s owners slowly become the owners of every domain’s data plumbing, without the domain knowledge that would make the job possible.

Test 3. Does this definition live with its domain? Machinery with no domain knowledge in it is shared; an endpoint, a parser, a schema for one domain’s data travels with the domain.

Duplication is cheaper than a dependency between teams

In the next post the party remote starts holding state of its own, and its first need is Pokémon data for a tapped slot: data the Pokédex domain already knows how to fetch. The tidy instinct proposes a shared data package: one definition, two consumers, no drift. The series does the untidy thing instead and has the party team write its own endpoint file. It comes to about twenty lines and looks like a failure of housekeeping. The file it duplicates, the Pokédex’s own, trimmed to its shape:

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) };
      },
    }),
  }),
});

Count what the tidy version costs. A data package shared between two feature teams is a dependency between peers. Every change to it lands on both teams, so every change needs both teams’ agreement. Releases chain: bump the package, wait for the other team to take the bump, then ship the thing you actually wanted to ship. And the package itself needs an owner: one of the two teams answers for its bugs, or nobody does. A shared definition between peers does not remove the coordination; it converts drift you can see into scheduling you have to do. The series has already photographed what a versioned dependency between teams looks like when the schedules diverge. Post 5’s loose peer range let one team’s app pair an old contract with a new screen, every compiler stayed green, and a user got a sentence with a hole in it:

The Pokémon detail screen reading 'Opened from the' with nothing after it, because the list app holds an older contract version and never passed the field the newer screen renders

Twenty duplicated lines cost twenty lines. Each copy follows its own team’s needs and ships on its own team’s dates, and the drift between them has a hard limit, because both parse the same wire format from the same backend. When the duplication grows past a file (five endpoints, ten), the answer changes, and the next section says how to tell.

The coupling test

The duplication rule needs a limit, because teams couple to shared code all the time and are right to: React, the navigation runtime, baseApi, the contract types. The line between those and the peer data package is the most reusable test in this essay.

Test 4. Does this dependency already exist, or does sharing invent it? Couple to what you already depend on; refuse the dependency that sharing itself creates.

A client generated from the backend’s API specification is fine to share, because it couples its consumers to the backend, a dependency every one of them already has. When the specification changes, every consumer was affected the moment it changed; the generated client just surfaces the fact at build time instead of in production. The contract package passes the same test: params and module shapes are agreements the apps were already bound by implicitly, written down where every compiler can see them. The peer data package fails it. Nothing about the party domain depended on the Pokédex team’s release schedule before the shared package existed; the package is what would create that dependency.

duplication couples to what already existsthe backendPokédex: its endpoint fileparty: its twenty linesthe shared package invents a dependencyevery changeevery releasePokédex teamshared data packageparty team

The pull of the shell

Every rule so far pushes code outward, into domains and packages. One argument pushes the other way, and it deserves the longest hearing in the essay, because it is usually made by the most careful engineer in the room.

Endpoints multiply. Several teams fetch overlapping data. Someone proposes the obvious consolidation: move the shared capability up into the shell, where the platform team can hold one definition of everything. One HTTP layer, one auth story, one set of data definitions, consistent behaviour by construction, drift impossible. Nobody proposes this cynically. On pure technical grounds it delivers exactly what it promises, and one definition really is easier to reason about than four copies.

The costs arrive from two directions. The first is organisational, and it lands before any code is written: escalating a capability to the shell makes the platform team a precondition. A feature team that needs a shell-level change cannot start until another team schedules the work, builds it and ships it. Its start date now sits inside someone else’s sprint. Multiply that by every team with a request, and the platform team becomes a queue in front of the whole programme, while inheriting, request by request, domain knowledge it never owned and cannot keep current. The word to sit with is start: the feature team is stopped, and it stays stopped until a team with different priorities ships.

Test 5. Can the feature team start without another team shipping first? Escalating a capability to the shell fails this one by design.

The second cost is specific to mobile: the shell is the binary, so shell code ships at app-store cadence. Build, submit, review, staged rollout, then a long tail of users who never update at all.

A capability moved into the shell moves onto the app-store train, and a schema tweak that rides a store review is the exact cost this series adopted federation to escape. Escalation reintroduces it quietly, one capability at a time.

The platform layer genuinely earns one kind of code, whether it rides in the shell binary or in a platform-owned package like the contract: the slow-moving kind. The HTTP client, auth, baseApi’s machinery, observability, the runtimes every remote resolves against. The pattern across that list: capability that changes slowly, carries no domain knowledge, and was already depended on by everyone. The list grows rarely, and each addition needs an argument, because everything on it inherits the binary’s cadence.

Inner source: the workable middle

Between “every team waits on the platform team” and “no shared capability at all” there is a middle that large orgs have a name for: inner source. Run the shared code like an open-source project that happens to be internal: the shell, the contract package, a component library, a shared data layer if your org decides it wants one. The platform team are the maintainers and code owners. Product teams send pull requests.

The blocking precondition dissolves. A feature team that needs a shell-level change writes the change itself, against the shared repo’s contribution rules, and waits for a review instead of a roadmap slot. The platform team’s job shifts from build-everything to review-and-steward: it holds the bar on quality and coherence and stops being the queue. The Pokédex team adding a component to the shared library becomes a pull request, not a ticket in another team’s backlog. So does the party team’s schema addition, if a shared data layer is the choice your org made after all.

The costs are real. Review latency does not vanish; a contested pull request can wait as long as a roadmap slot ever did. The platform team ends up maintaining code it did not write, a genuine burden the day the contributing team moves on. And the model only works on real contribution infrastructure:

Before the first PRWhat inner source runs on
Contribution conventions a stranger can follow, written down
CI a contributing team can run without asking anyone
Ownership files that route each review to the right people

Inner source pays for itself when contributions come often enough to justify that overhead.

“Just open a PR” against an undocumented repo is a politeness, not a process. Without the contribution infrastructure, inner source is the queue with a friendlier name.

The tests, and what moves the answers

The whole argument fits on one map, each box holding what it owns and the cadence it ships at:

party domain: another teamPokédex domain: one teamplatform-ownedmounts at runtimemounts at runtimeinjects endpoints intopublished toinstalls the viewthe shellstore wiring · tab bar · sharedruntimesships as the app binary@pokedex/contracts: theagreementstypes · baseApi · tag namessemver; majors wait forconsentlist remotescreens + its two endpointsships any afternoon@pokedex/detaila view: props in, pixels outsemverparty remotethe grid; state in the next postships any afternoonregistrywhere the packages travel

Five tests, in the order the series met them:

The five testsWho owns what
1Does this boundary have a team behind it? Domains get remotes; screens get packages.
2Does this component know where its data comes from? A shared component renders what it is given; data arrives as props.
3Does this definition live with its domain? Machinery is shared; endpoints, parsers and schemas travel with the domain.
4Does this dependency already exist, or does sharing invent it? Couple to the backend you already depend on; refuse the peer package that manufactures a schedule dependency between teams.
5Can the feature team start without another team shipping first? If not, the capability sits at the wrong level, or the process around it does.
All five sit behind the gate: more than one team ships this app. If not, folders answer every one of them for free.

None of these produce one answer for every org; they tell you what each option costs in yours. Two teams that trust each other and release together can share a data package and barely feel the coupling. A platform team of one cannot review at the pace five teams contribute, and inner source turns back into the queue it was meant to replace. An app with three teams and a quarterly cadence might keep everything in the shell and never notice the price. If your org cut these lines differently and the seams hold, that is a different answer to the same tests, and worth defending on its own terms.

The next post puts the tests straight to work. The party grid has been empty since post 4, because nothing in the app can reach it. The party team is about to fill it with state it owns, and the first decisions on the way are exactly the ones this essay just argued: who owns the slice, where the endpoint file goes, what the contract carries. The rules stop being prose and start being files.

Sources

  • Micro Frontends — Cam Jackson’s survey, including the self-contained fragment pattern this essay argues against for mobile
  • Conway’s law — the 1968 paper: systems mirror the communication structures of the organisations that build them
  • InnerSource Commons — the practice, its patterns, and the contribution infrastructure it depends on
  • Team Topologies — Skelton and Pais on platform teams and stream-aligned teams, the vocabulary behind the shell debate
  • react-native-module-federation — the companion repo whose decisions this essay defends
Warren de Leon
Warren de Leon

Software Engineering Manager. Most recently led the Mobile Platform team at Hargreaves Lansdown. Writing about engineering leadership, React Native, and building great teams.

View profile