Asher Cohen
Back to posts

Frontend Architecture Is Not Missing. You're Just Looking at the Wrong Boundary.

Modern frontend hasn't abandoned separation of concerns—it has adapted it to a medium where ephemeral state, lifecycle, and reactivity are first-class concerns.

I grew up remembering when WinForms applications were the buzzword. Early on, one principle was non-negotiable: never couple business logic to the UI. Forms were thin. Domain rules lived elsewhere. Separation of concerns wasn't academic—it was how you avoided unmaintainable systems.

So when people look at modern frontend code and see "logic in components," the reaction is predictable: have we forgotten everything we learned?

We haven't. But the boundary moved.

The Desktop Model and Why It Worked

In desktop systems, the UI was typically a projection over relatively stable, in-process state. The application owned the data. The form reflected it. Lifecycles were long-lived. The environment was controlled. Separation worked cleanly because state was durable and centralized, and controllers or presenters could coordinate it without much friction. The UI was thin because it could afford to be thin.

+-------------------+
|       UI (Form)   |
|  - Rendering      |
|  - Event wiring   |
+---------+---------+
          |
          v
+-------------------+
|   Controller /    |
|   Presenter       |
+---------+---------+
          |
          v
+-------------------+
|   Domain Model    |
|   Business Logic  |
|   Durable State   |
+-------------------+

Long-lived process · In-memory stable state · UI as projection layer · Clean vertical separation

The Web Changed the Ground Rules

It introduced a category of state that traditional MVC never modeled well: ephemeral, interaction-bound state. Focus, hover, selection, input drafts, optimistic updates, loading and error states, derived projections like filtering or sorting, and the synchronization of remote data with latency and caching. This state is transient, deeply tied to lifecycle, frequently recomputed, and often owned by a specific subtree of the UI. It is not durable domain state. But it is essential.

Early web development tried to ignore this. We posted forms and lost state. Then we serialized it back and forth. Then we invented mechanisms to preserve it on the server. Eventually, as interactivity increased and SPAs emerged, the tension became impossible to ignore. Client-side MVC frameworks carried forward assumptions that no longer held: centralized controllers, passive views, stable models. But in the browser, UI lifecycles are granular and nested. Rendering is reactive and frequent. State ownership is localized. Network latency is structural.

The result was increasing coordination complexity. Two graphs emerged: the UI composition tree and the state dependency graph. We kept trying to separate them cleanly. In practice, they overlap.

                   (Network)
                       |
                       v
                +---------------+
                |   Server API  |
                +---------------+
                       ^
                       |
+--------------------------------------------------+
|                    Browser                       |
|                                                  |
|   +-------------------------------------------+ |
|   |   UI Subtree                              | |
|   |  - Local state  <--> Derived state        | |
|   |  - Focus/hover  <--> Optimistic state     | |
|   |  - Draft inputs <--> Loading/error        | |
|   +-------------------------------------------+ |
|                                                  |
+--------------------------------------------------+

Ephemeral interaction state · Remote data with latency · Frequent recomputation · Lifecycle-bound ownership

The Two-Graph Problem

UI Composition Tree:          Reactive State Graph:

App                           Server Data --> Filtered List
├── Page                             |               |
│   ├── Sidebar                      v               v
│   └── Content               Cache Layer    Selected Item
│       ├── List                                     |
│       └── Detail                                   v
                                             Derived View State

      (UI Node) <----depends on---- (State Node)

These are not separate layers. They intersect at multiple points. The graphs overlap structurally.

The Component Model Was an Admission

The component model was less a revolution and more an admission. The view and its local state are often a unit. Not because we abandoned architecture, but because strict separation was increasing accidental complexity. A modern component is not merely a template. It acts more like a scoped ViewModel: it owns interaction state, manages derived projections, orchestrates lifecycle-bound effects, and binds all of that to rendering. That is interaction logic, not domain logic.

+--------------------------------+
|         Component              |
|--------------------------------|
|  Rendering (View)              |
|  Local interaction state       |
|  Derived state                 |
|  Effects (lifecycle-bound)     |
+--------------------------------+

App
├── Component A (owns A-state)
└── Component B (owns B-state)

State ownership aligns with lifecycle boundaries.

Classic MVC on the Client

Before the component model, centralized MVC produced a structural mismatch. Ephemeral state had no natural home:

+-------------------+
|   Controller      |
|  (Singleton-ish)  |
+---------+---------+
          |
          v
+-------------------+
|       Model       |
+---------+---------+
          |
          v
+-------------------+
|        View       |
+-------------------+

But ephemeral state lives here:
  [ Focus ] [ Input draft ] [ Loading ] [ Hover ]

Not durable Model.
Not global Controller.
Not pure View.

This mismatch caused coordination complexity.

This distinction matters. Serious frontend systems still separate domain rules. Pricing logic, validation rules, workflow invariants—these should live in pure modules, independent of rendering frameworks. Application orchestration can sit above them, composing domain logic with data fetching. Infrastructure handles API clients and caching. Components bind state to markup and manage ephemeral interaction concerns. When that structure exists, the presence of logic inside components is not architectural decay. It is intentional layering.

+--------------------------------------+
|              UI Layer                |
|  Components (interaction state)      |
+--------------------+-----------------+
                     |
                     v
+--------------------------------------+
|        Application / Orchestration   |
|  Hooks / Services                    |
|  Compose domain + remote data        |
+--------------------+-----------------+
                     |
                     v
+--------------------------------------+
|            Domain Layer              |
|  Pure logic                          |
|  No framework imports                |
+--------------------+-----------------+
                     |
                     v
+--------------------------------------+
|          Infrastructure Layer        |
|  API clients, caching, retries       |
+--------------------------------------+

Old mental model: Domain -> Controller -> View Modern mental model: Domain -> Orchestration -> UI Subtrees <- Remote Infrastructure

The Maturity Gap

The confusion arises because not every team enforces those boundaries. Frontend has the lowest barrier to entry in the industry. It optimizes for fast feedback and shipping velocity. Developers can be productive in a narrow slice without understanding the larger system. That accessibility is a strength, but it also means architectural maturity varies widely. What people often interpret as "frontend has no architecture" is frequently a maturity gap in specific codebases.

Co-location won not because we forgot separation of concerns, but because we learned where separation actually reduces complexity. If a piece of state exists solely to drive interaction in a subtree and dies when that subtree unmounts, centralizing it increases coordination cost. It introduces unnecessary indirection, registration overhead, and synchronization bugs. Architecture is not about maximal separation; it is about minimizing accidental complexity.

Without co-location:

UI Subtree -----> Global Store -----> UI Subtree
        \                              /
         ------- synchronization ------

With co-location:

UI Subtree
   |
   +-- owns its interaction state

Reduced: registration complexity · synchronization bugs · cognitive overhead

The UI Is a System in Its Own Right

Modern frontend acknowledges that UI is no longer a thin projection. It owns interaction, mediates remote data, performs derivations, manages latency, and handles partial failure. The UI is a system in its own right. Components are the containment strategy for that system.

So when someone says frontend lacks architecture because logic lives in components, the critique is aimed at the wrong boundary. The desktop model assumed a stable, durable core with a passive UI shell. The web environment distributes responsibility differently. The separation still exists—but it sits between domain logic, application orchestration, infrastructure, and UI subtrees rather than between controller and view.

We did not abandon architectural thinking. We adapted it to a medium where ephemeral state, lifecycle, and reactivity are first-class concerns. Once you accept that reality, the component model stops looking like careless coupling and starts looking like an honest reflection of the problem space.

Desktop:

[ Stable Core ]
       ^
       |
     Thin UI

Modern Web:

[ Domain Core ]
        ^
        |
[ Orchestration Layer ]
        ^
        |
[ Rich, Stateful UI System ]
  - Its own lifecycle
  - Its own state graph
  - Its own failure modes

Components are not architectural decay. They are the containment mechanism for that system.

#frontend #architecture #react #softwareengineering #ui #ux #design #engineering