The Janitor Pattern
Second-class citizen, first-class problem.
There is a role that does not appear in any job description but exists on almost every frontend team. You know it when you are living it. You spend more time navigating someone else’s decisions than making your own. Every new feature requires archaeology. Every bug fix risks destabilizing something three layers removed. You are not building anymore. You are maintaining the illusion that the codebase is still under control.
This is the Janitor Pattern. And it is not caused by bad developers.
It is caused by a structural assumption that gets made early, often before the first line of code is written, and almost never gets questioned: that the frontend is a UI layer. A skin over the real system. Something you bolt on at the end, staff with whoever is available, and optimize last if at all.
That assumption is wrong. And the codebase you inherit six months later is the proof.
How the Assumption Takes Hold
It rarely starts as a conscious decision. It starts as a staffing model.
The backend engineers get senior architects. They get ADRs, design reviews, and ownership over the data layer. The frontend gets whoever is left, or whoever was cheapest, or whoever interviewed well enough to clear the bar for “can ship components.” The implicit message is clear even when the explicit message is not: this part does not require serious engineering.
From there, the assumption compounds. No architectural investment means no architectural standards. No standards means every engineer makes their own local decisions. Local decisions, made independently over time, produce a system with no coherent shape. State leaks across boundaries that should not share state. Components grow to accommodate every use case that ever touched them. A component that was written to render a data table gradually absorbs filtering logic, then sorting state, then a network call, then an inline error handler, until it is four hundred lines long and nobody can confidently change it without running the full application to see what breaks. Business logic migrates into the render tree because there is nowhere else to put it and nobody with the authority to put it somewhere better. The codebase becomes a record of every compromise that was made under pressure, and none of the intentions that preceded them.
Leadership looks at the result and concludes that frontend is just messy by nature. It is not. It is messy because no one was given the mandate, the time, or the authority to keep it otherwise.
What It Actually Costs
The first cost is velocity, and it compounds in a way that does not show up cleanly on any roadmap.
Early on, the team ships fast. The codebase is small, everyone knows where everything is, and there is no meaningful technical debt because there is not yet enough code to have debt. Leadership sees the speed and attributes it to the team. What they are actually seeing is the absence of consequences. The debt is being taken on, not yet being repaid.
Six months later, the same features take twice as long. A year later, they take four times as long. But the team is also larger now, which obscures the signal. The productivity loss per engineer is invisible because it is distributed across more engineers. What leadership perceives as a scaling problem, we need to hire more, is actually an architectural problem: the system is resisting change because it was never designed to accommodate it.
The second cost is engineer quality. Senior engineers tolerate janitor work for a while. They do not tolerate it indefinitely. The ones who leave are almost never the ones who cause the mess. They are the ones who recognize it, who tried to address it, and who eventually concluded that the organization had no interest in addressing it with them. What remains tends toward the engineers who either do not see the problem or have stopped caring. Neither produces a better codebase.
The third cost is invisible and therefore the most dangerous: the features you do not build. Every hour spent managing accumulated complexity is an hour not spent on the product. No one tracks this. It does not appear in sprint velocity or quarterly reports. It exists only as a gap between what the team could have shipped and what it actually shipped, and leadership usually fills that gap with explanations that have nothing to do with the real cause.
Frontend Is a System
The framing of “frontend” as a UI layer is not just organizationally convenient. It is technically incorrect.
A modern frontend application manages state, enforces business rules, handles asynchronous coordination, defines data contracts with backend services, implements security boundaries in the form of auth flows and guarded routes, and owns the full error surface that users ever actually see. It makes rendering decisions, caching decisions, and navigation decisions. In a sufficiently complex product, the frontend state machine is as sophisticated as anything running on the server.
Treating that as a “layer” is not a simplification. It is a category error.
The same engineering discipline that applies to backend systems applies here: clear separation of concerns, explicit data flow, bounded ownership, and components that do one thing well. The difference is that on the backend, these principles are treated as table stakes. On the frontend, they are treated as optional, or worse, as overhead.
SOLID is not a backend concept. A component that renders a form, validates input, manages submission state, handles error display, and fires analytics events is violating the single responsibility principle just as clearly as any god object in a Java service layer. It is harder to test, harder to change, and harder to reason about. The fact that it is written in JSX rather than Java does not make the violation less real.
KISS applies too. Not in the reductive sense of “write simple code,” but in the sense of resisting the pull toward abstraction before abstraction has earned its complexity budget. Frontend codebases are full of “flexible” systems that no one uses flexibly, generic components that handle twelve variants when they needed to handle two, and configuration-driven rendering logic so abstracted that adding a new case requires reading three files before writing one line.
YAGNI is where frontend most consistently goes wrong. The instinct to generalize too early, to build the reusable version before the requirements are even stable, produces abstractions that are wrong in ways you cannot see until you try to use them. The cost of premature abstraction in a UI codebase is not just the code that was written. It is the constraints that code imposes on every decision that comes after it.
Component design, specifically, benefits from a discipline most frontend teams never formalize: keep components as dumb as possible for as long as possible. A component that receives props and renders output is easy to test, easy to compose, and easy to change. A component that also manages its own network state, its own business logic, and its own error recovery is none of those things. The only state a component should own by default is the state that is genuinely local to its rendering, whether a dropdown is open, whether a tooltip is visible. Everything else belongs somewhere else, managed by something whose job is to manage it.
This is not a new idea. It is not even a frontend idea. It is what engineering discipline looks like when it is applied consistently. The problem is not that frontend engineers do not know this. It is that the organizations they work in do not create the conditions for it.
What Disciplined Frontend Architecture Looks Like
So what does the alternative actually look like? Not as a set of principles, but as working code you can open and read.
I built a Battleship game as a public project specifically to demonstrate what frontend architecture looks like when the same standards applied to backend systems are applied to the frontend without compromise. The game itself is not the point. The architecture is.
The domain layer, hit detection, sunk-ship resolution, turn management, game-over detection, lives in pure TypeScript functions under engine/ and services/. No React imports anywhere in that layer. The same reducers that power the browser UI also drive a standalone CLI runner that you can invoke with npm run cli. That is not a demo added afterward. It is proof the layer boundaries are real: a second consumer in a completely different runtime environment, Node terminal versus browser DOM, works without changing a single line of domain code. If the domain had leaked React, the CLI would not compile.
State is minimal by design. Each game hook persists only what cannot be derived from other state: the shots map and the last shot result. Everything else, whether a cell is hit, whether a ship is sunk, whether the game is over, is computed via useMemo from those two facts. There is one source of truth per fact. The alternative, persisting derived values alongside the raw state, creates a second source of truth that must be kept consistent with the first. When those two diverge, and they will, the UI renders incorrect state. Deriving is consistent by construction.
Shot state transitions are atomic. Firing a shot must update the shots map and the last result together. Two useState calls would create a window between the first and second update where the component has inconsistent state: the shot is recorded but the result is still the previous one, or vice versa. An aria-live region reading during that window announces the wrong result. A single useReducer dispatch eliminates the window entirely. One dispatch, one new state object, no intermediate renders.
When a second game mode was added, it required a new engine module, a new hook, and a new wiring component. No existing rule logic was touched. Services, utilities, and the original single-player engine were extended, not rewritten. That is the practical payoff of clear layer boundaries: new requirements have a predictable place to go, and change is local rather than global.
Components are dumb by default. The Board component knows how to render a grid and manage keyboard navigation. It does not know what a ship is, what a shot is, or what the rules are. It could render a Minesweeper grid without changing its implementation. The only components that call hooks are the two wiring components at the top of the game tree, and those components contain no logic of their own. This is not a heroic constraint. It is what components look like when someone decided early what they are for.
None of this required a new framework, a state management library, or a methodology. It required deciding, before the first component was written, that the frontend was a system with the same expectations applied to any other system, and that those expectations would be enforced.
A production codebase is harder. The requirements are less clear, the team is larger, the deadlines are real. But the failure mode is not that production constraints make architecture impossible. The failure mode is that no one was asked to do it, or given the authority to enforce it, or protected from the pressure to skip it.
What Needs to Change
For engineering leadership, the starting point is recognizing that the staffing model is the architecture.
If you staff the frontend with engineers who are not expected to own the architecture, no architecture will be owned. If you do not create review processes that include frontend design decisions alongside backend ones, frontend design will not be reviewed. If you treat frontend performance, frontend accessibility, and frontend code quality as secondary concerns to be addressed “after we ship,” they will remain secondary concerns indefinitely, because there is always something else to ship.
The investment required is not enormous. It is a senior engineer with genuine ownership and the authority to make and enforce architectural decisions. It is design review that includes the frontend. It is the same expectation of discipline that you already apply to the systems you have decided matter.
For senior frontend engineers, the work is not just building better systems. It is making the cost of bad systems legible to the people who control the conditions under which systems are built. That means tracking the time lost to architectural debt explicitly, not absorbing it silently into estimates. It means framing refactoring work in terms of shipping velocity, not code quality, not because code quality does not matter, but because the argument has to land somewhere that registers. It means being willing to push back on the framing that frontend is inherently messy, because that framing is false and accepting it makes the problem permanent.
The Janitor Pattern persists because it is self-obscuring. The people creating the conditions for it rarely see the consequences directly. The engineers absorbing the consequences rarely have the standing to name the cause. Breaking that loop requires both sides to be honest about what is actually happening.
The frontend is not a UI layer. It is a system. It deserves to be treated like one.

