A Mental Model for Data in UI Applications
TL;DR: UI = f(transport(model(storage)))
This is the high-level mental model I’ve developed for thinking about how data flows through UI applications. It attempts to describe a technology-agnostic model of the layers that naturally arise in any reasonably sized application, and the abstract properties of each layer. Like any useful model it is lower fidelity than reality. Still, I’ve found it useful for reasoning about where in the stack different problems should, or should not, be solved, and what tradeoffs we make when we elide one or more of these layers.
Storage Layer #
(e.g. Postgres, NoSQL, Redis, Microservices, IndexDB, SQLite)
The Storage Layer houses your data in its durable, normalized, form. The shape of the stored data should be focused on space and retrieval efficiency. All duplication should be removed. Data that can be computed should either live in an explicit cache storage or left to be derived on the fly. Indexes and foreign keys should be optimized for performance not semantics.
Model Layer #
(e.g. Ent, ORM)
The Model Layer should provide a mapping from the data in its storage shape to its product context shape. You should have objects/classes/fields/methods that match the mental model of how you want developers and users to be thinking about the data in your product. This layer should be denormalized, meaning that commonly derived fields (full name, etc.) and aggregations (total account spend) should be modeled in this layer.
For most applications this data will form a graph where different data types (nodes) have semantic relationships (edges) to one another.
Transport Layer #
(e.g. REST, GraphQL, tRPC)
Note: In server rendered applications this layer can often be bypassed because the UI layer has direct access to the Model Layer.
The Transport Layer allows clients which want to render a UI to ask the server for a slice of data. This layer generally wants to describe its data needs at the same semantic abstraction as the model layer, except that it wants to fetch just a tree-shaped subset of the graph. As a rule, the size of this tree should be bounded by the amount of data that can be shown on a single page.
UI Layer #
(e.g. htmx, Swift UI, React, Jetpack Compose)
The UI Layer takes the tree-shaped slice of product-context data from the Transport Layer and maps it to a tree of UI elements that present the data in a way that is tailored to a specific job-to-be done.