# The cross-platform boundary: share what must not diverge

> "Which cross-platform framework" is the wrong first question. The right one is which layers must be identical on both platforms — because divergence there is a bug — and which should differ, because divergence there is correct.

- Canonical (HTML): https://kaguc.com/blog/cross-platform-boundary/
- Date: 2026-08-28


*Also in [中文](/blog/cross-platform-boundary-zh/). Part 4 of [Trading app architecture](/blog/trading-app-architecture/).*

The cross-platform debate is usually conducted as a framework comparison, which is why it never resolves. The prior question decides most of it: **which parts of your app must be identical on iOS and Android, and which parts should not be?**

Answer that and the framework choice narrows to a couple of reasonable options. Skip it and no amount of benchmarking will help, because you are arguing about tools while the requirement is undefined.

## The test: is divergence a bug or a feature?

Take any layer of your app and ask what happens if the two platforms behave differently.

For an order validator, a fee calculation, an options pricing rule, a state machine, a protocol encoder: divergence is a **defect**. Two independent implementations of the same financial rule *will* drift — not maybe, will. They drift on the edge cases first, which in this domain are also the expensive cases. Nobody notices for months, and the bug report arrives as a customer whose numbers didn't match.

For navigation transitions, gesture physics, date pickers, permission flows, accessibility, share sheets: divergence is **correct**. Users have platform expectations, and code that flattens them produces an app that feels subtly wrong on both. Effort spent unifying this layer buys negative value.

That's the whole rule: **share the layer where difference is a defect, keep native the layer where difference is the point.**

Notice this is a correctness argument, not an efficiency one. DRY is usually justified as "write it once," which invites the counterargument that duplication is sometimes cheaper. That counterargument is fine for UI and irrelevant for domain rules, because there the cost of duplication isn't typing — it's two divergent truths about money.

## What sharing actually costs

Sharing is not free, and the costs are structural rather than obvious:

**A boundary appears.** Shared code and platform code must talk. That interface needs types both sides understand, serialization, and error mapping. It is now a thing to design and version.

**A runtime arrives.** A second runtime means memory, startup cost, and a debugger that no longer sees the whole picture. When something breaks *at* the boundary, you are debugging two systems and the seam between them.

**The team splits.** Someone must be fluent in the shared layer and its toolchain. At small scale that is one person, which is a bus factor rather than a plan.

So the question isn't "does sharing help" — it's "does the divergence risk in this layer exceed the boundary cost." For domain logic, usually yes. For a settings screen, usually no.

## The three positions

**Fully native, twice.** Maximum platform fidelity, zero boundary cost, and every rule written twice. Reasonable when the shared logic is thin or the platform surface is most of the product.

**Share the logic, keep native UI.** The domain, models, and validation live in one codebase; each platform renders it. This targets exactly the layer where divergence is a defect and leaves the layer where it isn't. The cost is that boundary and its toolchain.

**Share the UI too.** One codebase draws both platforms. Fastest to a feature-complete app with one team, and the reason most small teams should start here. The cost arrives later and from a predictable direction: platform capabilities. Every camera, biometric, background, or notification feature eventually needs a plugin, and a plugin is native code with extra steps. Budget for that from the start instead of being surprised by it.

## The mixed model, and what it really costs

Large apps often end up mixed: a native shell with a cross-platform UI framework embedded for some screens. This is usually not indecision — it is the honest shape of a migration, and it's the right pattern when you cannot stop shipping to rewrite ([more on that next](/blog/modular-evolution/)).

It has real costs worth naming before you choose it:

- **Runtime memory.** Each embedded engine has a footprint. Multiple live engines multiply it, and the mitigation — caching and reusing engines — is itself a lifecycle problem you now own.
- **Two navigation stacks.** The native stack and the framework's router must be unified, or back-button behavior becomes a permanent source of bugs.
- **State on both sides.** Session, theme, and locale now exist twice and must be kept in agreement. Every duplicated piece of state is a future desync.
- **Debugging across runtimes.** A stack trace stops at the boundary. Crash and performance tooling must be configured to see both sides, or half your incidents become guesswork.

## Make the boundary coarse

Whatever you share, the interface between the shared core and the platform should be **coarse-grained**.

The cost of crossing a boundary — serialization, marshalling, a runtime transition — is paid per *call*, not per byte. A chatty interface with dozens of small property reads can be dramatically slower than one call returning a complete object, even though it moves less data. This is the same lesson as batching updates at a native boundary in [part one](/blog/realtime-data-pipeline/), and it generalizes: **design boundaries as few, meaningful operations, not as remote field access.**

There's a design benefit too. A coarse interface expresses intent ("place this order," "give me this screen's data"), which stays stable while implementations change underneath. A fine-grained interface exposes structure, and structure changes constantly.

## Choosing, honestly

Two variables decide it: how much of your product is **domain logic that must not diverge**, and how many **people** you have.

- One small team, mostly UI over an API: share everything. One codebase, native modules where the platform demands it.
- Small team, meaningful domain logic: share everything, but keep that logic in its own layer with no UI dependencies — so it can be extracted later without a rewrite.
- Multiple platform teams, substantial domain logic: share the logic, keep UI native. This is where the boundary cost is clearly repaid.
- Single platform: the question does not exist. Don't pay for optionality you have no evidence you'll use.

The mistake that costs the most is choosing a sharing strategy for a team size you don't have yet. Architecture that anticipates an organization you might build is how small teams end up maintaining a large team's overhead with a small team's capacity.


## References

- J. Waldo, G. Wyant, A. Wollrath, S. Kendall, ["A Note on Distributed Computing"](https://scholar.harvard.edu/files/waldo/files/waldo-94.pdf), Sun Microsystems Laboratories TR-94-29, 1994 — a boundary between address spaces cannot be papered over; latency and partial failure are paid per call.
- A. Hunt, D. Thomas, [*The Pragmatic Programmer*](https://pragprog.com/titles/tpp20/), 1999 — DRY, stated as a rule about knowledge, not about typing.
- M. E. Conway, ["How Do Committees Invent?"](https://www.melconway.com/Home/Committees_Paper.html), *Datamation*, 1968 — system structure mirrors the communication structure of the organization that built it.
- Flutter docs, [Add Flutter to an existing app](https://docs.flutter.dev/add-to-app) and [Multiple Flutter instances](https://docs.flutter.dev/add-to-app/multiple-flutters) — engine warm-up, `FlutterEngineGroup`, and the cost of several live engines.

---

*Previous: [Local-first data](/blog/local-first-sync/) · Next: [Modularity and migration](/blog/modular-evolution/)*

