# Trading app architecture: six constraints and what they force

> A trading client's architecture is mostly settled before you choose a stack — by data rate, irreversibility, catalog size, platform count, team count, and an untrusted device. Here is what each one forces, and how the design collapses when your version of the constraint is smaller.

- Canonical (HTML): https://kaguc.com/blog/trading-app-architecture/
- Date: 2026-08-28


*Also in [中文](/blog/trading-app-architecture-zh/). A six-part series.*

You cannot pick a trading client'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't control.

So the useful question is not "which stack." It is: **what does each of those constraints force, and what happens to the design when your version of that constraint is smaller?**

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'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.

## The six constraints

**1. Freshness.** 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 *wrong* data. This forces a pushed, sequenced, back-pressured transport, and a decision about which streams may be conflated and which must be resynced.
→ [Real-time data pipelines: push, backpressure, and conflation](/blog/realtime-data-pipeline/)

**2. Irreversibility.** An order cannot be undone by the user, and exactly-once delivery does not exist. This forces an idempotency key minted and persisted *before* the first send, an explicit unknown state on timeout, reconciliation instead of blind retry, and a server that is the only source of truth.
→ [Order reliability: idempotency, unknown states, and reconciliation](/blog/order-reliability/)

**3. A large catalog.** 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.
→ [Local-first data: snapshots, deltas, and the two kinds of state](/blog/local-first-sync/)

**4. Two platforms, one truth.** iOS and Android must agree exactly on financial rules, and should *not* 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.
→ [The cross-platform boundary: share what must not diverge](/blog/cross-platform-boundary/)

**5. Many teams, many years.** 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.
→ [Modularity and migration: cutting boundaries that survive](/blog/modular-evolution/)

**6. An untrusted device.** 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.
→ [The client security model: an input, never an authority](/blog/client-security-model/)

## Flutter implementation notes

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.

- [Flutter under a firehose](/blog/flutter-highfreq-state/) — per-symbol notifiers, frame coalescing, and making rebuild scope match change scope
- [Channels, Pigeon, and FFI](/blog/flutter-native-channels/) — the three ways out of Dart, memory ownership, and why `EventChannel` has no backpressure
- [Charts that hold 60fps](/blog/flutter-chart-rendering/) — custom painting, static and live layers, and an honest `shouldRepaint`
- [Shipping a database with your app](/blog/flutter-offline-store/) — bundled SQLite, data version vs schema version, FTS with pinyin search
- [Project structure and add-to-app](/blog/flutter-project-structure/) — packages as enforced boundaries, routing as a registry, what embedding costs
- [A reference design: quotes MVP](/blog/flutter-trading-mvp/) — a committed plan, what it deliberately doesn't build, and the trigger for each upgrade

## The decision rules

Every part above ends with "adopt it when a measurement demands it," 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.

It also draws the distinction the rest of the series glosses over: **correctness has no threshold.** 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.

- **[When to adopt the heavy machinery: thresholds, not opinions](/blog/adoption-thresholds/)**

## Which of these are actually yours?

Each requirement came from a constraint, and constraints have magnitude. Shrink one and the requirement it produced collapses into something much smaller:

| If your reality is… | The requirement… | becomes… |
|---|---|---|
| Data changes at human speed | pushed, sequenced, conflated pipeline | a WebSocket into UI state |
| Actions are reversible | idempotency key, unknown state, reconciliation | a request with a retry |
| Small or server-side catalog | on-device snapshot + delta sync | a search endpoint |
| One platform, or thin domain logic | shared cross-platform core | build it once, natively |
| One small team | module registry, ports and adapters | one codebase, no enforced boundaries |
| Low-value or public data | layered hardening | standard auth and TLS, done properly |

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 *correct* architecture, and the large one would be a mistake.

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.

## How to use this

Write down your own six magnitudes honestly. Work out what each one forces. Build only that.

Then adopt each heavy element on the day a *measured* 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.

What compounds is the map from constraints to structure. The structure itself is disposable, and someone else's structure, copied, is a liability with good PR.


## References

- D. L. Parnas, ["On the Criteria To Be Used in Decomposing Systems into Modules"](https://doi.org/10.1145/361598.361623), *CACM* 15(12), 1972 — a module should hide a decision that is likely to change.
- 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.
- J. H. Saltzer, D. P. Reed, D. D. Clark, ["End-to-End Arguments in System Design"](https://web.mit.edu/Saltzer/www/publications/endtoend/endtoend.pdf), *ACM TOCS* 2(4), 1984 — a guarantee is only real at the endpoint that can verify it.
- M. Kleppmann, [*Designing Data-Intensive Applications*](https://dataintensive.net/), O'Reilly, 2017 — ch. 5 (replication) and ch. 11 (stream processing) are the background for the catalog and feed designs.
- J. Bezos, [Amazon 2015 Letter to Shareholders](https://s2.q4cdn.com/299287126/files/doc_financials/annual/2015-Letter-to-Shareholders.PDF), 2016 — Type 1 (one-way door) vs Type 2 (two-way door) decisions.

---

*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.*

