Where State Actually Changes

In CRUD screens, the core problem is often not where state is stored, but whether the mutation path is defined. Once load, input, save, and reload are allowed to diverge, the screen becomes difficult to reason about.

This is the eleventh post in Problems Cotomy Set Out to Solve. This continues from Synchronizing UI and Server State .

In the previous post, I described how Ajax made UI and server state drift apart once load and save stopped sharing one contract. The next problem is closely related, but more specific:

the real failure is usually not where state is stored. It is that the mutation path is scattered.

Server Rendering Was Cruder, but Structurally Safer

In systems built with server-side rendering and ordinary POST, major structural failures were less likely to appear. That model had many usability limits. Browser history could become awkward. Progress indication during submission was limited. Input error handling also had fewer options than modern asynchronous screens.

Even so, the operational path was relatively honest. The same request flow usually loaded the screen, accepted input, validated the result, and rendered the next state. That did not guarantee correctness, but it reduced one important category of failure: the same Entity was less likely to be interpreted through several unrelated update paths.

Even so, I now think simple CRUD-oriented business systems should still use Ajax in many cases. The interaction model is usually better. But once Ajax is introduced, the implementation must become more disciplined. Otherwise the screen gains flexibility at the price of structural drift.

The Core Problem Is Mutation Path, Not State Location

It is easy to describe the problem as state duplication across the server and the client. That is not wrong, but it is not the deepest issue.

Data can exist in several places without immediately causing failure. The real problem begins when the system no longer defines where changes are allowed to enter.

In a typical CRUD screen, the same Entity can change through several paths:

flowchart TB
    Load["Initial load"]
    Input["User input"]
    Save["Form save"]
    Reload["Ajax reload"]
    UI["Local UI interaction"]
    Entity["Same Entity state"]

    Load --> Entity
    Input --> Entity
    Save --> Entity
    Reload --> Entity
    UI --> Entity

If those paths are independent, the screen becomes difficult to reason about. One path updates visible inputs. Another updates display-only DOM. Another updates server-side truth. Another patches the screen after save.

At that point, the issue is not simply that state is distributed. The issue is that mutation entry points are distributed.

That is the structural definition I wish I had much earlier. The screen becomes unreliable when the same thing can change from many places without a shared contract.

A mutation path is the defined entry point through which state is allowed to change. If this is not defined, the system is not just complex. It is structurally ambiguous. Without enforcing this boundary, every handler becomes a potential mutation path, and the system silently loses its structure.

The Frontend Starts Learning Too Much

Once Ajax-based load and save are introduced, one familiar pattern appears very quickly. The frontend starts knowing more about data structure than it should.

At first, this looks harmless. A screen loads JSON, then jQuery or another client-side layer places values into inputs and display blocks. But that means the client now needs to know property names, value shape, and special handling rules.

Then the same screen saves through another path. Validation rules may still live on the server, but the client already knows enough about the data shape that it starts collecting more logic around it. Soon the DTO is effectively declared twice: once in the server-side contract, and again in the frontend behavior that fills, reads, and patches the screen.

Part of the reason this bothered me so much is that I had already worked with three-tier client-server systems around SOAP. Those systems also declared DTO-like structures in multiple places. But the contract was at least communicated through WSDL, so each side had a more explicit way to share the same structure and generate or verify types against it.

That did not remove every integration problem, of course. But the structure felt more formally announced. Compared with that, Ajax-heavy Web screens often looked as if the same data shape was being reinterpreted locally without an equally strong contract. If someone had worked through those older three-tier systems, I think this difference is fairly easy to recognize.

That is where structure begins to collapse. The server should hold authoritative data and business rules. The frontend should mainly handle presentation and input assistance. But once the frontend becomes a second interpreter of the same Entity, the change surface is no longer readable.

The immediate cost is not always a dramatic bug. More often, it appears when fields are added, when display rules change, or when one endpoint response evolves slightly. The problem is maintenance visibility. No one can easily see the full impact range of a change.

Component Frameworks Narrow One Kind of Chaos

I did look into React and Vue when I became unable to tolerate continued frontend growth around jQuery and older alternatives. I did not end up using them in real work, so this is not an implementation report from production use.

Even so, one point was already clear to me. A component-oriented framework is a strong help when the frontend itself is large and complex. Hiding direct DOM manipulation behind a state-driven rendering model makes the screen easier to understand.

This is especially true when the frontend is not just a thin screen layer, but an independent large system in its own right. In that kind of architecture, the frontend is not merely a visual continuation of one server-side business flow. It becomes its own substantial runtime structure, with its own internal screen composition, state transitions, and coordination burden.

In that situation, a component model is very persuasive. It gives the frontend an internal architecture that can stand on its own. That is different from a business screen whose main role is still to connect server-side operations, input, and display through one relatively narrow request-and-response-oriented flow.

When data loaded through Ajax is expanded into the screen, it is easier to understand a model where memory state changes first and the UI follows that change. The actual implementation is never as simple as the idea sounds, but the idea itself is still easier to reason about than many direct DOM patches.

This matters because a component model often narrows mutation paths inside the frontend. State change is expected to enter through a smaller number of routes. That is a real architectural advantage.

Component frameworks reduce chaos inside the frontend by narrowing mutation paths. But they do not define how those paths relate to server-side state. As a result, the system may be internally consistent on the client, while still being structurally split across the application. They define how state changes inside the frontend, but not where change should enter the system as a whole.

But it does not solve the whole business-screen problem. It mainly solves frontend-internal consistency. Server synchronization is still a separate issue. And in CRUD-centered systems, forcing every screen into a large SPA model can split one business function into two architectures: server-side business flow and frontend application flow.

That split is often too heavy for the actual job. Many business systems do not need a massive SPA. What they need is a way to keep asynchronous load, fill, save, and render within one understandable structure.

One reason I developed and published Cotomy was that I could not find many options that fit that middle space well enough.

Most Business Screens Fall Into a Small Number of Types

In ordinary business systems, screens usually belong to one of three categories.

There are screens for searching data. There are screens for showing and editing one record. And there are screens that do not fit either shape and must be designed more individually.

Search screens should usually be server-rendered in the first place. One major reason is that search conditions and paging fit naturally into the query string. That means the current search can be retained in the URL, revisited later, and shared with someone else without inventing a separate client-side state model.

That property matters a great deal in business systems. Search is often not just an interaction. It is also a reference point that needs to be reopened, bookmarked, compared, or passed to another person.

For that kind of screen, a server-side query flow is usually the more natural architecture. There is often little reason to pay extra frontend complexity for something that already fits the Web’s ordinary address model.

Of course, after arriving through that URL, the client can still perform additional filtering or other small interactions if necessary. But search screens usually do not require the frontend to become an independent application with its own heavy coordination model. In most cases, it is more natural for the search and the rendering to happen within the same request that the URL already represents.

Edit screens are different. That is where Ajax becomes more useful, because save behavior, validation feedback, and user interaction benefit much more directly from asynchronous flows.

Special screens still need individual design.

But the important point is not the category label by itself. The important point is that each category should have a defined mutation path. If a search screen, an edit screen, and a special coordination screen all mutate data through arbitrary local handlers, then the category distinction no longer helps the architecture. The purpose of this classification is not categorization itself, but to ensure that each type of screen has a predictable mutation path. Otherwise, the same Entity ends up being mutated through unrelated mechanisms depending on the screen type.

jQuery Made the Mutation Problem Hard to Name

When I was first building these kinds of edit screens with jQuery, I wrote the logic that expanded loaded data directly into the screen. That meant the client-side code had to know the data format explicitly. Even if field names rarely changed, adding properties meant updating several places.

So I moved toward automatic filling through attributes such as name and related screen metadata. That idea itself is not unusual. It is a fairly ordinary way to reduce repetitive screen code.

The real value was not just convenience. It was that screen-specific code became smaller, while common fill behavior could be applied across many screens through one route. The server continued to decide the data shape. The frontend no longer needed custom field-by-field expansion for every form.

The problem is that jQuery still did not give me a satisfying structural unit for this. Common behavior and screen-specific behavior kept drifting apart. Page-level behavior and form-level behavior were hard to treat as one readable object. Updates were event-driven, local, and easy to add, but hard to classify.

That is the main reason jQuery-based screens became difficult for me. The problem was not simply syntax. The problem was that state change entry points were hard to define clearly. The difficulty was not writing code, but identifying where change was supposed to happen.

Naming the Flow Was More Important Than DRY

JavaScript has become much easier to use in an object-oriented style than it used to be. If the language environment had looked then as it does now, I might not have felt the same pressure to adopt TypeScript so early.

But at that time, given the browser environments my clients actually used, class-style JavaScript was much less readable in practice. With jQuery, event-oriented code still looked more approachable, even though it was structurally weaker.

After I started building the predecessor of Cotomy in TypeScript, I began standardizing forms at a fairly early stage. That work was not only about code reuse. It was about assigning names and meaning to the flow of CRUD processing.

Load, fill, submit, validate, and render should not exist as accidental local procedures. They should exist as recognized mutation paths.

That distinction matters. When a structure is named, it becomes easier to preserve. When it is preserved, a larger system can still be edited by a small team without each screen turning into a private architecture.

How Cotomy Tries to Keep the Path Understandable

This is the context in which Cotomy’s form structure matters to me. CotomyForm defines one submission lifecycle. CotomyApiForm turns that into an API submission path using FormData. CotomyEntityApiForm keeps the Entity key on the form and adjusts the request method based on whether identification already exists. CotomyEntityFillApiForm then loads data through the API when the page is ready, fills matching inputs, and projects values to display-oriented DOM through its renderer.

This does not remove all complexity. It does something narrower and more practical. It tries to keep update paths recognizable.

The point is not that state must live in only one place. The point is that mutation should enter through a small number of named structures instead of scattered handlers.

That was the real design pressure. I did not just want less repetitive code. I wanted fewer unofficial ways for one screen to become current. The goal is not abstraction, but to prevent mutation from escaping into unnamed paths.

Conclusion: Define the Entry Point of Change

The deeper problem in CRUD screens is not merely client state, server state, or DOM state existing at the same time. The deeper problem is that the same Entity is often allowed to change through too many unrelated routes.

Server-side postback systems hid many UX problems, but they at least kept one main execution path. Ajax-based systems improve interaction, but only if mutation paths are defined well enough that load, fill, save, and render still belong to one understandable model.

That is the axis I consider most important now. The question is not only where state is stored. The question is where state is allowed to change. A system without defined mutation entry points is not just hard to maintain. It is impossible to reason about. State can be duplicated. Mutation paths cannot be undefined.

Problem Series

This article is part of the Cotomy Problem Series, which examines recurring structural failures in business UI design.

Series articles: HTML and CSS as One Unit , Form Submission as Runtime , Screen Lifecycle and DOM Stability , Form State and Long-Lived Interaction , API Protocols for Business Operations , Runtime Boundaries and Operational Safety , UI Intent and Business Authority , Binding Entity Screens to UI and Database Safely , Screen State Consistency in Long-Lived UIs , Synchronizing UI and Server State , and Where State Actually Changes.

Next

Next planned: Why Business Systems Rarely Need SPA Frameworks

Learn Cotomy

Cotomy is a DOM-first UI runtime for long-lived business applications.