Cross-Platform Code Is a Tax You Pay Twice
Cross-Platform Code Is a Tax You Pay Twice
The cross-platform code debate has been running for fifteen years and most teams answer it wrong.
It is a quarterly conversation at every mobile org I have been at. Someone in leadership, often well-intentioned, looks at the two mobile teams (iOS, Android) shipping features in parallel and asks the obvious question: why are we doing this work twice? Surely we should share code. Look at how much we duplicate.
The default reflex is to share code between iOS and Android to save effort. The reflex is wrong. Most teams that act on it pay that effort twice: once in the abstraction layer they have to build and maintain, once in the special-cases the abstraction never quite handles. The cases where cross-platform actually pays off are narrower than the discourse pretends. I want to make the case for the narrow position, because the loud position is going to drag a lot of teams into bad architectural decisions this year.
The fifteen-year history in one paragraph
Titanium in 2009. PhoneGap in 2009. Xamarin in 2011. React Native in 2015. Flutter in 2017. Each generation arrived with a credible pitch: write once, run on both platforms, save engineering effort. Each generation had a real adoption wave. Each generation had a string of high-profile retrenchments where teams that bet on it walked it back. The discourse has a structural amnesia about this. Every cycle, the new framework's proponents argue that "this time is different, the previous frameworks were primitive, the architecture has matured." Every cycle, the same operational problems show up at the same scale of adoption, with the same set of workarounds, and the same eventual mixed verdict.
We are in the seventh cycle now. Flutter 2 shipped in early March with Google pushing the maximal pitch: not just iOS plus Android, but five operating systems plus the web. "The same codebase to ship native apps to five operating systems." It is the most ambitious version of share-everything that has ever been pitched in this space. The companion bet from JetBrains, Kotlin Multiplatform Mobile, went alpha last August with a deliberately narrower pitch: share business logic, keep UI native. Same problem, opposite philosophies, both gaining traction simultaneously.
If you only read the marketing, you would think the question has finally been solved. The history says be skeptical.
The math nobody runs
Here is the calculation almost no team does before committing.
A typical cross-platform pitch goes: "iOS engineer time and Android engineer time together is N hours per quarter. We can save 60% of that with shared code." The savings number is always between 30% and 70%, depending on who's pitching. The savings line item makes the spreadsheet look obvious.
The line items the spreadsheet doesn't show:
The integration layer. The native shell that exposes platform APIs to your cross-platform runtime. Camera, push notifications, biometrics, deep linking, background processing, share extensions, app clips, widgets. Every platform-specific feature has to be either implemented twice in native shims or done without. The integration layer is its own codebase. It is also itself a third platform, which is the next line item.
The third-platform problem. Once you have iOS, Android, and "the cross-platform layer," you have three runtime environments to maintain, not two. Bugs reproduce on one or two but not all three. Test matrices triple. Hiring needs a third skill set. Airbnb's June 2018 retrospective on sunsetting React Native is now three years stale, but the line that did not age is the framing that they had "three platforms to maintain instead of two." That cost is real and persistent. Every published large-company retrospective on cross-platform names it.
The platform divergence cost. iOS and Android are not converging. They are diverging. iOS 14 added widgets and App Clips. Android 11 added bubbles and conversation shortcuts. Apple ships App Tracking Transparency (ATT); Google ships Privacy Sandbox. Every annual platform release adds APIs that don't have an obvious cross-platform abstraction. You either degrade to the lowest common denominator (and your iOS users get a worse product than the native competition) or you build platform-specific code paths inside the shared layer (and you are now back to doing the work twice, inside an extra abstraction).
The special-case tax. Every cross-platform team I have watched at scale ends up with twenty to forty percent of their code as "iOS-only" or "Android-only" inside the shared codebase. The savings math assumed close to zero. The reality is the special-case tax compounds with every platform release.
When you actually run the math with the line items included, the savings number from the pitch deck drops from 50% to something closer to 15%. Sometimes it is negative, when you include the cost of training the team into the new toolchain.
Discord did it right by doing it asymmetrically
The most useful production data point I have seen on this is Discord's writeup of running React Native on iOS only, from late 2019. They run React Native on iOS, with three core engineers. They explicitly refuse to bring it back to Android after a 2018 trial. The decision is asymmetric on purpose: share where the platform JS engine and team composition make it pay, go native where it doesn't.
The principle generalizes. Cross-platform code-sharing is not a binary. The right answer can be one platform, not both. The right answer can be "share three specific subsystems" rather than "share the app." The right answer almost always involves saying no to the maximalist version of the bet.
Most teams cannot bring themselves to do this because the architectural argument was framed as a yes/no. Once it's yes, sharing everything is the default. Sharing selectively requires a separate decision that nobody on the cross-platform team is incentivized to make.
The three cases where cross-platform actually pays
I am not arguing against cross-platform code. I am arguing against the default reflex. The narrow cases where cross-platform pays off:
Business logic that is genuinely platform-agnostic and changes frequently. Pricing rules, content recommendation logic, fraud detection scoring, A/B test cohort assignment. The logic does not touch UI. The logic ships through the network anyway. The logic is updated weekly. Sharing it across iOS and Android pays back the abstraction cost quickly. This is the Kotlin Multiplatform Mobile pitch in its honest form, and it is the case I find most defensible. Netflix's writeup of their Studio apps on KMM from last October is the cleanest production case study; they share "significant business logic, entirely platform agnostic" and explicitly exclude UI.
Internal tools and admin apps that don't need native polish. The internal employee app. The admin dashboard. The annual reorg tool. Anything where the user is a coworker, the surface is workflow-shaped, and "feels native" is not load-bearing. React Native and Flutter both ship perfectly fine internal tools.
When the alternative is not shipping. If you don't have the staffing to do native well, the question is not "cross-platform vs. native." It is "ship cross-platform or don't ship at all." That answer is usually "ship cross-platform" even with the integration tax. The math is different when the alternative is no product.
These three cases share a structure: the shared code is well-bounded, the failure modes are tolerable, and the team is honest about what they are giving up.
What Apple's Catalyst tells you
I want to bring in one more data point that's instructive. Apple itself tried to ship a cross-platform framework with Catalyst, bringing UIKit apps to the Mac. John Gruber's December 2019 review, "Catalyst, Two Months In", is worth reading even now. His conclusion is that "just check a box in Xcode" promise collapses on contact with platform conventions. Keyboard support, window behavior, idiomatic controls, all need bespoke work for the second platform. Apple, the platform owner, with full access to the runtime and toolchain, could not make its own cross-platform framework deliver on the share-everything pitch. I argued in February 2020 that iOS teams should not ship Catalyst apps that year; the argument was the same shape as this one, in a narrower domain.
If Apple can't make Catalyst work cleanly between iPad and Mac, two platforms they own end to end, the burden of proof on Flutter or React Native promising to do it cleanly between iOS and Android, two platforms they own neither of, is heavy.
What I would tell a mobile leader in Q2 2021
If you are running mobile engineering and the cross-platform conversation is on your roadmap:
Frame the question as "which subsystems," not "which platform stack." The yes/no question is the wrong question. The subsystem question is the right question.
Run the line-item math. Build the spreadsheet that includes the integration layer cost, the third-platform cost, the platform-divergence cost, and the special-case tax. If the savings number from the pitch deck doesn't survive contact with the line items, you have your answer.
Look at three production case studies before committing. Airbnb's retrospective is the canonical caution. Discord's asymmetric usage is the canonical pragmatic. Netflix's KMM ship is the canonical narrow-share. Any decision that doesn't engage with all three is making the call with insufficient information.
Be honest about the tax you are paying for cross-platform UI. It exists. It compounds. iOS users notice. Android users notice. Your competitors who go native will out-ship you on platform-specific surfaces. If you are okay with that trade in exchange for engineering velocity elsewhere, fine. If you assumed there was no trade, you are about to learn the hard way.
Where I land
Cross-platform code is a tax you pay twice because the savings number from the pitch is missing the most expensive line items. The fifteen-year history of this debate has produced a clear pattern: the largest published share-everything retrenchments have come within three years, and asymmetric or narrow-share bets compound. The reflex to share is the wrong starting position. The starting position should be "iOS team and Android team ship in parallel, and we share specific subsystems where the math clearly says yes." That is a smaller, less exciting architectural call. It is the one that survives contact with three years of platform evolution.
The teams that internalize this in 2021 are going to look very smart in 2024. The teams that fall for the Flutter 2 maximalist pitch are going to spend 2023 walking it back, the same way the teams that fell for React Native in 2016 walked it back in 2018. Same story, new framework name.