Previous article: Small-Team Development Without Losing Continuity
Why I Started Thinking About This
I work inside a company that is not an IT company. The company runs several kinds of business, including field work such as store cleaning and building maintenance, and also the manufacture and sale of household goods.
Over the last few years, the household goods business has grown. That growth created a very practical problem. The amount of work increased faster than the company could realistically increase the number of people doing that work. As a result, a large part of my own job became the development of systems for that manufacturing and sales operation.
The system I built for that area is, by far, the largest system I have built mostly by myself. I use AI heavily now, so saying by myself needs some qualification, but the ownership of the design, implementation direction, and maintenance still sat with me.
Even if I list only the parts that come to mind quickly, the system includes customer management, product management, orders, quotations, delivery processing, shipment-related data import from parcel carriers, import from EC sites, and import from EDI systems operated by individual customers. Each function is not especially difficult in isolation. The order area has some complicated details, but most parts are still ordinary business-system work. The scale came less from one difficult algorithm and more from the accumulation of screens and operational cases. In the end, it became a system with more than one hundred screens.
That experience changed how I think about frontend architecture for internal systems. In particular, it made me much less willing to start from the assumption that a SPA is the natural modern answer.
Why I Considered a SPA
This was not a conclusion I had from the beginning. Before building that larger system, I was working on a simpler system for field operations such as cleaning work. It handled schedules, reports, and sharing problems found at job sites. Calling it a large system would make it sound bigger than it was, but it was still a real operational tool.
Around that time, the earliest shape of what later became Cotomy was also starting to emerge. I wanted to improve the behavior of business screens, especially around form handling, page behavior, and small dynamic interactions. I also considered making notifications richer. For example, when a new order was received, it seemed useful to notify people more actively. Most of those notification ideas were later dropped because the cost did not match the practical value, but at the time they made me consider using React or a similar framework and building the system as a SPA.
That was a reasonable idea to consider. SPA frameworks are strong when the frontend itself becomes a substantial application. They provide mature component models, state handling patterns, routing, ecosystem support, and many ways to create a rich interface quickly relative to the quality of the resulting UI. For certain products, that is exactly the right trade-off.
But once I organized the requirements, I abandoned the SPA direction.
The Core Was Database Operation
The reason was simple. The essential nature of the system was database operation.
That may sound too obvious, but I think it matters. For most of this system, the work was not an attempt to create one large interactive frontend experience. It was a collection of functions that searched, displayed, created, updated, and deleted business records. Customers, products, quotations, orders, delivery data, imports, and status transitions all had their own rules, but the basic shape remained close to the data model.
The difficult part was not usually the visual interaction itself. It was the business rules behind the screen. This may be especially true in Japan, or at least in the kinds of business relationships I have seen, but each company often has its own rules, habits, formats, closing times, confirmation steps, and exceptions. Even inside one sales operation, the system may need to absorb differences by customer. Some differences appear in order data. Some appear in CSV import. Some appear in EDI import. Some appear in shipment decisions or delivery handling. It may sound surprising if you have not worked inside this kind of operation, but some companies may ask for a delivery note, which would normally be attached to the shipped goods, to be sent later by email instead. Changing the shipper name on a parcel label to the name of an intermediary company is also not unusual. These are not interesting frontend problems. They are operational rules that the system has to remember because people should not have to rely on memory for them every time.
It is not realistic to automate every difference perfectly. But leaving too much of that work to memory and manual attention can cause serious mistakes. An overlooked exception can become a wrong shipment, a missed delivery date, a billing error, or a customer-specific promise that is not followed. So the development cost had to go into deciding which differences the system should absorb, where it should warn the user, and where it should prevent an operation entirely. Compared with that work, making the whole application a richer frontend surface was not the highest-value place to spend complexity.
This does not mean the UI was unimportant. In daily order processing, the screen has to make the right things visible quickly. People need to understand which orders must be shipped today, which orders are still open, what state each order is in, and where attention is needed. Those parts are not solved by merely putting a table on a page. The design has to protect recognition. It has to let the operator read the situation without doing unnecessary mental work.
For example, for order and delivery status, I also adopted representations other than ordinary tables. The actual screen cannot be published here, but the idea is simple. The statuses are shown as panels, progress is separated by color, and the same view is available not only on terminals and PC screens at hand, but also on a large monitor visible from the work area. The people doing shipment work can look up at that monitor at any time and understand, even from a distance, roughly how far the day’s work has progressed. The actual operations after that are still done on terminals at hand. But the shared display changed the way the work was perceived. People no longer had to open a separate report or ask someone else what was still left before deciding what to do next. As a result, it became easier for them to start working on the remaining tasks more actively. In that kind of screen, the important point is not that the interface is technically rich. The important point is that the current operational state remains visible enough that people can keep working without repeatedly reconstructing the situation in their heads.
But that requirement does not automatically require a SPA. It requires careful screen design. Those are different questions.
AJAX Is Not the Same as SPA
Some screens do need dynamic behavior. Loading existing entity data into an edit form, replacing part of a screen, submitting a form without losing the surrounding context, or refreshing one area independently can all be useful in business applications.
Cotomy itself reflects that need. Its API form classes submit form data through the API layer, and CotomyEntityFillApiForm can load existing entity data and fill form fields. That is a real part of the implementation, not only a design idea. The point, however, is that this kind of AJAX-based behavior does not require the whole application to become a SPA. This also influenced the direction Cotomy took. It grew around page-level business screens and small dynamic behavior, rather than around replacing the whole application with a client-side application.
A screen can be server-rendered as the main unit and still use dynamic loading where it helps. Several regions on a page can be loaded separately when that is useful. An edit form can be filled from an API. A local operation can update the visible state without navigating away. None of those choices force the entire system into a client-side routing and component architecture.
At the same time, I do not think selective AJAX means adding dynamic behavior everywhere. If one screen needs many independent AJAX regions, complex client-side coordination, and long-lived local state, then that screen may no longer fit this page-oriented style very well. In that sense, SPA frameworks probably developed partly to control the complexity that appears when AJAX-heavy interfaces grow too large. The important question is not only whether an application is technically a SPA. It is how much business state, screen state, and temporary interaction state the frontend is expected to own.
React and similar frameworks are very good at managing large stateful component trees. If the screen really is a large, continuously interactive frontend application, that strength matters. But in an internal business system, I think that kind of screen should be treated carefully. In many internal business systems, if a screen reaches that level of complexity, I think it is worth asking whether the work should be divided differently.
The Value of Small Screens
What worked for me was much more ordinary. I built most screens as simple combinations of tables, forms, and page-level actions. The screens were not primitive, but they were intentionally narrow. One screen usually had a recognizable business purpose, and the surrounding files stayed close to that purpose.
That made the system much cheaper to build than it would have been if I had treated the frontend as one large application. More importantly, it made the system easier to understand.
When one function is small, the amount of context needed to change it is smaller. The data model and the screen usually correspond to each other closely enough that finding the relevant place is not difficult. For AI-assisted coding, I think this matters even more than it first appears. AI agents work from limited context. If a change can be made within a compact screen boundary, the agent has less room to miss a distant dependency or silently change behavior in an unrelated part of the system.
In practice, this means I can often define the working area in operational terms. Customer-related behavior belongs mainly to customer management screens. Product master behavior belongs mainly to product management screens. Shipment status behavior belongs mainly to shipment screens. Order processing is not completely independent from customers, products, or shipments, of course. But even then, the entry point and the main responsibility can usually be placed around a screen or a small group of related screens. That makes it easier to ask an AI agent to work inside the right area, and easier for me to review whether it stayed there.
This is not only an AI issue. Human developers benefit from the same structure. When most screens follow the same basic shape, the ordinary screens become cheap. That leaves more attention for the places where UI quality really matters: daily order handling, shipment work, exception cases, status recognition, and other screens that operators touch constantly.
I do not mean that SPA architecture prevents focus. It does not. But page-level vertical slicing makes the choice more explicit. Most screens can stay ordinary, and the important screens can receive more design effort without forcing the whole system into the same level of frontend complexity.
There is also a maintenance advantage. When features are split by screen and business function, a change is less likely to damage unrelated areas. In team development, that kind of division can also make ownership easier. People can work on different functional areas with less accidental interference. The same is true when AI is involved. Smaller feature boundaries make both implementation and review easier.
What SPA Frameworks Are Good At
I do not want this to sound like a rejection of SPA frameworks. That would be the wrong conclusion.
If the product has a simple domain but needs a highly polished interface, a SPA can be an excellent choice. If the screen itself is the product, if interaction quality is the main value, or if the user experience depends on continuous client-side state, then a rich frontend framework may produce a better result in less time than trying to assemble the same behavior manually.
The problem is not that SPA frameworks are bad. The problem is using SPA thinking as the default answer for systems whose real complexity is elsewhere.
In a large internal business system, the complexity is often spread across many small operational rules, many data-entry paths, many status cases, many imports, and many screens. The hard part is not always making one screen extremely rich. The hard part is making one hundred screens consistent enough, understandable enough, and changeable enough that the system can keep following the business.
For that problem, a more traditional screen-by-screen structure can be very strong.
The Cost of Applying SPA Thinking Everywhere
SPA architecture also changes the shape of the project. It usually makes the boundary between frontend and server-side behavior more explicit. That can be good. It can also create extra coordination work.
When the frontend grows horizontally across many functions, shared components, shared state assumptions, routing patterns, and client-side abstractions can start to connect areas that were originally separate in the business. Once that happens, the team has to decide how components should be divided, which abstractions belong where, and how much business meaning should be allowed into the frontend layer.
Those decisions are not impossible. Good frontend teams handle them every day. But they are real costs, and I do not think every internal system environment is prepared to pay them.
This feels especially visible in Japan, at least from my own experience. The web frontend world and the business application development world overlap, but they are not the same culture. Business application projects often have people who understand databases, forms, reports, operations, and customer-specific workflows better than they understand modern frontend architecture. That is not a defect. It is the background of the work. If such a team is asked to build everything through SPA assumptions, the project may spend too much effort managing the frontend structure itself instead of solving the business problem.
Again, this is not an argument against React or against modern frontend development. It is an argument against adopting their costs without checking whether the system needs their strengths.
Choosing the Older Shape Deliberately
For the system I had to build, giving up the SPA approach was the right decision. The system became large, but each part stayed small enough to understand. Most screens were ordinary. The important screens received more attention. Dynamic behavior was added where it had practical value. The structure also worked well with AI-assisted development because the context for each change usually stayed compact.
Looking back, even after the system grew beyond one hundred screens, most individual screens did not need to become large client-side applications. Each screen usually had a clear operational purpose: check a customer, edit a product master, process an order, review shipment status, or import data. Users usually opened a screen already knowing what work they intended to do there. Because of that, making the right screen easy to find from the menu and making the opened screen easy to understand mattered more than merging many operations into one rich interactive surface. Some parts still benefited from AJAX, but that did not change the basic usefulness of screen-level structure.
I do not think my case is unusual enough to treat it as an exception. Many internal systems have the same basic conditions. They are CRUD-heavy. They have many screens. They are maintained for a long time. They are often built by a small group of assigned developers, or by people gathered from outside the company whose skills, habits, preferences, and level of ownership do not necessarily match. The people responsible for maintenance may also change over time. And their real complexity often lives in business rules, customer-specific exceptions, status transitions, imports, and operational judgment rather than in one highly interactive frontend experience.
That does not make the approach universally correct. If I were building a small product with a simple backend and a demanding interactive UI, I would consider a SPA much more seriously. If the business value depended mainly on fluid client-side interaction, I would not try to force everything into old page boundaries.
But for many internal systems, especially CRUD-heavy systems with many screens and limited development capacity, I think it is worth reconsidering older development methods. Not because they are old. Not because modern frontend frameworks are wrong. But because screen-by-screen development often matches the actual shape of the work.
When cost, maintainability, operational clarity, and the ability to keep changing the system matter more than building one rich frontend surface, a non-SPA architecture can be a very rational choice. The important decision is not whether SPA is modern or traditional pages are old. The important decision is where the complexity of the system really lives, and whether the architecture is helping that complexity stay manageable.