<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Mobile | kaguc — Writing to understand systems.</title><link>https://kaguc.com/tag/mobile/</link><atom:link href="https://kaguc.com/tag/mobile/index.xml" rel="self" type="application/rss+xml"/><description>Mobile</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Fri, 28 Aug 2026 00:00:00 +0000</lastBuildDate><image><url>https://kaguc.com/media/logo.svg</url><title>Mobile</title><link>https://kaguc.com/tag/mobile/</link></image><item><title>The cross-platform boundary: share what must not diverge</title><link>https://kaguc.com/blog/cross-platform-boundary/</link><pubDate>Fri, 28 Aug 2026 00:00:00 +0000</pubDate><guid>https://kaguc.com/blog/cross-platform-boundary/</guid><description>&lt;p>&lt;em>Also in &lt;a href="https://kaguc.com/blog/cross-platform-boundary-zh/">中文&lt;/a>. Part 4 of &lt;a href="https://kaguc.com/blog/trading-app-architecture/">Trading app architecture&lt;/a>.&lt;/em>&lt;/p>
&lt;p>The cross-platform debate is usually conducted as a framework comparison, which is why it never resolves. The prior question decides most of it: &lt;strong>which parts of your app must be identical on iOS and Android, and which parts should not be?&lt;/strong>&lt;/p>
&lt;p>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.&lt;/p>
&lt;h2 id="the-test-is-divergence-a-bug-or-a-feature">The test: is divergence a bug or a feature?&lt;/h2>
&lt;p>Take any layer of your app and ask what happens if the two platforms behave differently.&lt;/p>
&lt;p>For an order validator, a fee calculation, an options pricing rule, a state machine, a protocol encoder: divergence is a &lt;strong>defect&lt;/strong>. Two independent implementations of the same financial rule &lt;em>will&lt;/em> 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&amp;rsquo;t match.&lt;/p>
&lt;p>For navigation transitions, gesture physics, date pickers, permission flows, accessibility, share sheets: divergence is &lt;strong>correct&lt;/strong>. 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.&lt;/p>
&lt;p>That&amp;rsquo;s the whole rule: &lt;strong>share the layer where difference is a defect, keep native the layer where difference is the point.&lt;/strong>&lt;/p>
&lt;p>Notice this is a correctness argument, not an efficiency one. DRY is usually justified as &amp;ldquo;write it once,&amp;rdquo; 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&amp;rsquo;t typing — it&amp;rsquo;s two divergent truths about money.&lt;/p>
&lt;h2 id="what-sharing-actually-costs">What sharing actually costs&lt;/h2>
&lt;p>Sharing is not free, and the costs are structural rather than obvious:&lt;/p>
&lt;p>&lt;strong>A boundary appears.&lt;/strong> 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.&lt;/p>
&lt;p>&lt;strong>A runtime arrives.&lt;/strong> A second runtime means memory, startup cost, and a debugger that no longer sees the whole picture. When something breaks &lt;em>at&lt;/em> the boundary, you are debugging two systems and the seam between them.&lt;/p>
&lt;p>&lt;strong>The team splits.&lt;/strong> 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.&lt;/p>
&lt;p>So the question isn&amp;rsquo;t &amp;ldquo;does sharing help&amp;rdquo; — it&amp;rsquo;s &amp;ldquo;does the divergence risk in this layer exceed the boundary cost.&amp;rdquo; For domain logic, usually yes. For a settings screen, usually no.&lt;/p>
&lt;h2 id="the-three-positions">The three positions&lt;/h2>
&lt;p>&lt;strong>Fully native, twice.&lt;/strong> 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.&lt;/p>
&lt;p>&lt;strong>Share the logic, keep native UI.&lt;/strong> 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&amp;rsquo;t. The cost is that boundary and its toolchain.&lt;/p>
&lt;p>&lt;strong>Share the UI too.&lt;/strong> 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.&lt;/p>
&lt;h2 id="the-mixed-model-and-what-it-really-costs">The mixed model, and what it really costs&lt;/h2>
&lt;p>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&amp;rsquo;s the right pattern when you cannot stop shipping to rewrite (&lt;a href="https://kaguc.com/blog/modular-evolution/">more on that next&lt;/a>).&lt;/p>
&lt;p>It has real costs worth naming before you choose it:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Runtime memory.&lt;/strong> 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.&lt;/li>
&lt;li>&lt;strong>Two navigation stacks.&lt;/strong> The native stack and the framework&amp;rsquo;s router must be unified, or back-button behavior becomes a permanent source of bugs.&lt;/li>
&lt;li>&lt;strong>State on both sides.&lt;/strong> Session, theme, and locale now exist twice and must be kept in agreement. Every duplicated piece of state is a future desync.&lt;/li>
&lt;li>&lt;strong>Debugging across runtimes.&lt;/strong> A stack trace stops at the boundary. Crash and performance tooling must be configured to see both sides, or half your incidents become guesswork.&lt;/li>
&lt;/ul>
&lt;h2 id="make-the-boundary-coarse">Make the boundary coarse&lt;/h2>
&lt;p>Whatever you share, the interface between the shared core and the platform should be &lt;strong>coarse-grained&lt;/strong>.&lt;/p>
&lt;p>The cost of crossing a boundary — serialization, marshalling, a runtime transition — is paid per &lt;em>call&lt;/em>, 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 &lt;a href="https://kaguc.com/blog/realtime-data-pipeline/">part one&lt;/a>, and it generalizes: &lt;strong>design boundaries as few, meaningful operations, not as remote field access.&lt;/strong>&lt;/p>
&lt;p>There&amp;rsquo;s a design benefit too. A coarse interface expresses intent (&amp;ldquo;place this order,&amp;rdquo; &amp;ldquo;give me this screen&amp;rsquo;s data&amp;rdquo;), which stays stable while implementations change underneath. A fine-grained interface exposes structure, and structure changes constantly.&lt;/p>
&lt;h2 id="choosing-honestly">Choosing, honestly&lt;/h2>
&lt;p>Two variables decide it: how much of your product is &lt;strong>domain logic that must not diverge&lt;/strong>, and how many &lt;strong>people&lt;/strong> you have.&lt;/p>
&lt;ul>
&lt;li>One small team, mostly UI over an API: share everything. One codebase, native modules where the platform demands it.&lt;/li>
&lt;li>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.&lt;/li>
&lt;li>Multiple platform teams, substantial domain logic: share the logic, keep UI native. This is where the boundary cost is clearly repaid.&lt;/li>
&lt;li>Single platform: the question does not exist. Don&amp;rsquo;t pay for optionality you have no evidence you&amp;rsquo;ll use.&lt;/li>
&lt;/ul>
&lt;p>The mistake that costs the most is choosing a sharing strategy for a team size you don&amp;rsquo;t have yet. Architecture that anticipates an organization you might build is how small teams end up maintaining a large team&amp;rsquo;s overhead with a small team&amp;rsquo;s capacity.&lt;/p>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>J. Waldo, G. Wyant, A. Wollrath, S. Kendall, &lt;a href="https://scholar.harvard.edu/files/waldo/files/waldo-94.pdf">&amp;ldquo;A Note on Distributed Computing&amp;rdquo;&lt;/a>, Sun Microsystems Laboratories TR-94-29, 1994 — a boundary between address spaces cannot be papered over; latency and partial failure are paid per call.&lt;/li>
&lt;li>A. Hunt, D. Thomas, &lt;a href="https://pragprog.com/titles/tpp20/">&lt;em>The Pragmatic Programmer&lt;/em>&lt;/a>, 1999 — DRY, stated as a rule about knowledge, not about typing.&lt;/li>
&lt;li>M. E. Conway, &lt;a href="https://www.melconway.com/Home/Committees_Paper.html">&amp;ldquo;How Do Committees Invent?&amp;rdquo;&lt;/a>, &lt;em>Datamation&lt;/em>, 1968 — system structure mirrors the communication structure of the organization that built it.&lt;/li>
&lt;li>Flutter docs, &lt;a href="https://docs.flutter.dev/add-to-app">Add Flutter to an existing app&lt;/a> and &lt;a href="https://docs.flutter.dev/add-to-app/multiple-flutters">Multiple Flutter instances&lt;/a> — engine warm-up, &lt;code>FlutterEngineGroup&lt;/code>, and the cost of several live engines.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;em>Previous: &lt;a href="https://kaguc.com/blog/local-first-sync/">Local-first data&lt;/a> · Next: &lt;a href="https://kaguc.com/blog/modular-evolution/">Modularity and migration&lt;/a>&lt;/em>&lt;/p></description></item><item><title>Trading app architecture: six constraints and what they force</title><link>https://kaguc.com/blog/trading-app-architecture/</link><pubDate>Fri, 28 Aug 2026 00:00:00 +0000</pubDate><guid>https://kaguc.com/blog/trading-app-architecture/</guid><description>&lt;p>&lt;em>Also in &lt;a href="https://kaguc.com/blog/trading-app-architecture-zh/">中文&lt;/a>. A six-part series.&lt;/em>&lt;/p>
&lt;p>You cannot pick a trading client&amp;rsquo;s architecture by taste. Most of it is decided before you write a line — by how fast the data changes, what a wrong order costs, how big the catalog is, how many platforms and teams you have, and the fact that the app runs on a device you don&amp;rsquo;t control.&lt;/p>
&lt;p>So the useful question is not &amp;ldquo;which stack.&amp;rdquo; It is: &lt;strong>what does each of those constraints force, and what happens to the design when your version of that constraint is smaller?&lt;/strong>&lt;/p>
&lt;p>That second half is the part worth caring about. Large financial apps converge on a recognizable shape, and the shape is often copied by teams who don&amp;rsquo;t share the constraints that produced it. What follows is the reasoning behind each element — informed by taking a large trading app apart and checking that the theory matched what a team at real scale actually built — and then the conditions under which each element is simply unnecessary.&lt;/p>
&lt;h2 id="the-six-constraints">The six constraints&lt;/h2>
&lt;p>&lt;strong>1. Freshness.&lt;/strong> Market data is a high-rate, unbounded stream whose value decays in milliseconds. It cannot be polled, the consumer is permanently slower than the producer, and a silently dropped update is not missing data but &lt;em>wrong&lt;/em> data. This forces a pushed, sequenced, back-pressured transport, and a decision about which streams may be conflated and which must be resynced.
→ &lt;a href="https://kaguc.com/blog/realtime-data-pipeline/">Real-time data pipelines: push, backpressure, and conflation&lt;/a>&lt;/p>
&lt;p>&lt;strong>2. Irreversibility.&lt;/strong> An order cannot be undone by the user, and exactly-once delivery does not exist. This forces an idempotency key minted and persisted &lt;em>before&lt;/em> the first send, an explicit unknown state on timeout, reconciliation instead of blind retry, and a server that is the only source of truth.
→ &lt;a href="https://kaguc.com/blog/order-reliability/">Order reliability: idempotency, unknown states, and reconciliation&lt;/a>&lt;/p>
&lt;p>&lt;strong>3. A large catalog.&lt;/strong> Hundreds of thousands of instruments, searchable instantly, including offline. This forces separating slow-changing reference data from fast-changing values, shipping a snapshot on the device, and syncing deltas against a monotonic version — while the ledger stays authoritative on the server.
→ &lt;a href="https://kaguc.com/blog/local-first-sync/">Local-first data: snapshots, deltas, and the two kinds of state&lt;/a>&lt;/p>
&lt;p>&lt;strong>4. Two platforms, one truth.&lt;/strong> iOS and Android must agree exactly on financial rules, and should &lt;em>not&lt;/em> agree on interaction idioms. This forces sharing the layer where divergence is a defect and keeping native the layer where divergence is correct — and paying attention to the cost of the boundary between them.
→ &lt;a href="https://kaguc.com/blog/cross-platform-boundary/">The cross-platform boundary: share what must not diverge&lt;/a>&lt;/p>
&lt;p>&lt;strong>5. Many teams, many years.&lt;/strong> The system must keep shipping while it changes shape. This forces module boundaries drawn around decisions likely to change, a core that receives its dependencies rather than reaching for them, and migration by growing the new inside the old instead of rewriting.
→ &lt;a href="https://kaguc.com/blog/modular-evolution/">Modularity and migration: cutting boundaries that survive&lt;/a>&lt;/p>
&lt;p>&lt;strong>6. An untrusted device.&lt;/strong> Every control you ship can be removed. This forces treating the client as an input and never an authority, keeping no secrets in the binary, auditing effective rather than declared policy, and treating every deep link and bridge call as hostile input.
→ &lt;a href="https://kaguc.com/blog/client-security-model/">The client security model: an input, never an authority&lt;/a>&lt;/p>
&lt;h2 id="flutter-implementation-notes">Flutter implementation notes&lt;/h2>
&lt;p>The six above are framework-neutral. This second group takes the same constraints down to a concrete Flutter build — with the patterns, the costs, and a reference design you could start from on Monday.&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://kaguc.com/blog/flutter-highfreq-state/">Flutter under a firehose&lt;/a> — per-symbol notifiers, frame coalescing, and making rebuild scope match change scope&lt;/li>
&lt;li>&lt;a href="https://kaguc.com/blog/flutter-native-channels/">Channels, Pigeon, and FFI&lt;/a> — the three ways out of Dart, memory ownership, and why &lt;code>EventChannel&lt;/code> has no backpressure&lt;/li>
&lt;li>&lt;a href="https://kaguc.com/blog/flutter-chart-rendering/">Charts that hold 60fps&lt;/a> — custom painting, static and live layers, and an honest &lt;code>shouldRepaint&lt;/code>&lt;/li>
&lt;li>&lt;a href="https://kaguc.com/blog/flutter-offline-store/">Shipping a database with your app&lt;/a> — bundled SQLite, data version vs schema version, FTS with pinyin search&lt;/li>
&lt;li>&lt;a href="https://kaguc.com/blog/flutter-project-structure/">Project structure and add-to-app&lt;/a> — packages as enforced boundaries, routing as a registry, what embedding costs&lt;/li>
&lt;li>&lt;a href="https://kaguc.com/blog/flutter-trading-mvp/">A reference design: quotes MVP&lt;/a> — a committed plan, what it deliberately doesn&amp;rsquo;t build, and the trigger for each upgrade&lt;/li>
&lt;/ul>
&lt;h2 id="the-decision-rules">The decision rules&lt;/h2>
&lt;p>Every part above ends with &amp;ldquo;adopt it when a measurement demands it,&amp;rdquo; which is meaningless until someone names the measurement. One article supplies the numbers — frame budgets, message rates, catalog sizes, team sizes — the ladder of cheaper fixes to exhaust first, and the counter-signals that should stop you even past a threshold.&lt;/p>
&lt;p>It also draws the distinction the rest of the series glosses over: &lt;strong>correctness has no threshold.&lt;/strong> Idempotency keys, server-side authorization, sequence numbers, and decimal money are day-one work at any scale, because the cost of retrofitting them is unbounded. Only performance decisions wait for a number.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://kaguc.com/blog/adoption-thresholds/">When to adopt the heavy machinery: thresholds, not opinions&lt;/a>&lt;/strong>&lt;/li>
&lt;/ul>
&lt;h2 id="which-of-these-are-actually-yours">Which of these are actually yours?&lt;/h2>
&lt;p>Each requirement came from a constraint, and constraints have magnitude. Shrink one and the requirement it produced collapses into something much smaller:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>If your reality is…&lt;/th>
&lt;th>The requirement…&lt;/th>
&lt;th>becomes…&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Data changes at human speed&lt;/td>
&lt;td>pushed, sequenced, conflated pipeline&lt;/td>
&lt;td>a WebSocket into UI state&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Actions are reversible&lt;/td>
&lt;td>idempotency key, unknown state, reconciliation&lt;/td>
&lt;td>a request with a retry&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Small or server-side catalog&lt;/td>
&lt;td>on-device snapshot + delta sync&lt;/td>
&lt;td>a search endpoint&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>One platform, or thin domain logic&lt;/td>
&lt;td>shared cross-platform core&lt;/td>
&lt;td>build it once, natively&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>One small team&lt;/td>
&lt;td>module registry, ports and adapters&lt;/td>
&lt;td>one codebase, no enforced boundaries&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Low-value or public data&lt;/td>
&lt;td>layered hardening&lt;/td>
&lt;td>standard auth and TLS, done properly&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Read the right column. For a small team building a quotes-and-watchlist MVP, this reasoning produces: one cross-platform codebase, REST plus a WebSocket, a search endpoint, no shared-core layer, no native performance work, no custom transport. That is not a simplified version of the large architecture. For those constraints it is the &lt;em>correct&lt;/em> architecture, and the large one would be a mistake.&lt;/p>
&lt;p>The large app is not more advanced. It is the same six constraints at full magnitude, plus years of accumulated migration state that no one would choose deliberately.&lt;/p>
&lt;h2 id="how-to-use-this">How to use this&lt;/h2>
&lt;p>Write down your own six magnitudes honestly. Work out what each one forces. Build only that.&lt;/p>
&lt;p>Then adopt each heavy element on the day a &lt;em>measured&lt;/em> constraint demands it — the queue actually grows, the profile actually points at the decode loop, the merge conflicts actually recur — and not one sprint earlier. Most of the cost of a large architecture is paid by teams who adopted it in anticipation of a scale they never reached.&lt;/p>
&lt;p>What compounds is the map from constraints to structure. The structure itself is disposable, and someone else&amp;rsquo;s structure, copied, is a liability with good PR.&lt;/p>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>D. L. Parnas, &lt;a href="https://doi.org/10.1145/361598.361623">&amp;ldquo;On the Criteria To Be Used in Decomposing Systems into Modules&amp;rdquo;&lt;/a>, &lt;em>CACM&lt;/em> 15(12), 1972 — a module should hide a decision that is likely to change.&lt;/li>
&lt;li>M. E. Conway, &lt;a href="https://www.melconway.com/Home/Committees_Paper.html">&amp;ldquo;How Do Committees Invent?&amp;rdquo;&lt;/a>, &lt;em>Datamation&lt;/em>, 1968 — system structure mirrors the communication structure of the organization that built it.&lt;/li>
&lt;li>J. H. Saltzer, D. P. Reed, D. D. Clark, &lt;a href="https://web.mit.edu/Saltzer/www/publications/endtoend/endtoend.pdf">&amp;ldquo;End-to-End Arguments in System Design&amp;rdquo;&lt;/a>, &lt;em>ACM TOCS&lt;/em> 2(4), 1984 — a guarantee is only real at the endpoint that can verify it.&lt;/li>
&lt;li>M. Kleppmann, &lt;a href="https://dataintensive.net/">&lt;em>Designing Data-Intensive Applications&lt;/em>&lt;/a>, O&amp;rsquo;Reilly, 2017 — ch. 5 (replication) and ch. 11 (stream processing) are the background for the catalog and feed designs.&lt;/li>
&lt;li>J. Bezos, &lt;a href="https://s2.q4cdn.com/299287126/files/doc_financials/annual/2015-Letter-to-Shareholders.PDF">Amazon 2015 Letter to Shareholders&lt;/a>, 2016 — Type 1 (one-way door) vs Type 2 (two-way door) decisions.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;em>The series is grounded in a static architectural analysis of a large trading app: what layers exist and how they communicate. No running app, no source recovery, no private protocols — and the detailed teardown notes stay out of the series deliberately, because the transferable part is the reasoning, not the artifact.&lt;/em>&lt;/p></description></item></channel></rss>