Every Decision Was 'Right' — Until the Project Rotted Anyway
Last year I took over a project. On the first day I flipped through the code and felt pretty good: components in components, hooks in hooks, requests in services, utilities in utils. The directory was as clean as a model home, every file in "its proper place".
Three months later, I was complaining to everyone about this directory structure. Not because it was messy — it wasn't messy at all; it was organized to the point of being useless.
The product manager raised a requirement: add a "corporate procurement" option at checkout. Sounded like adding a field. But when I actually started, I found that the pieces of "checkout" were scattered across six directories: the form in components/CheckoutForm.tsx, state in contexts/CheckoutContext.tsx, validation in utils/validateCheckout.ts, requests in services/orders.ts, types in types/order.ts, and even the logic to convert the form into an order lived alone in utils/formatPayload.ts.
Six directories, none named "checkout".
I changed things around, thinking I had covered everything. The night it went live, operations @-ed me in the group chat: corporate clients couldn't place orders at all. Checking, there was an old validator in utils written two years ago that also governed checkout. I had no idea it existed, so naturally I didn't change it. The fields allowed by the new form were vetoed by an old antique no one remembered.
I missed the change not because I wasn't careful, but because I had no way to be certain I hadn't missed anything.
1. The starting point of rot is often not "mistakes" but "correctness"
Later I saw a retrospective from a medical CRM team, and one phrase in it stopped me for a moment.
They called the problem they hit "Architecture Decision Degradation" (ADD). The definition was restrained: the gradual erosion of architecture quality through accumulated compromises. No dramatic collapse, just a slow crawl.
It's different from technical debt. Technical debt you borrow on purpose — ship first, pay back later. ADD is another matter: every decision the team made was defensible at the time, but a dozen "defensible" decisions stacked together push the architecture in a direction no one wanted.
What did that medical team do initially? Redux-Toolkit for centralized state, createAsyncThunk for async handling, optimistic updates to support drag-and-drop. Every step was "correct", code looked "really clean". Then the requirements came: WebSocket real-time updates, conflict detection, filtering by doctor. 3 slices became 10, and one drag operation touched 4 slices. Developers were asking in Slack: "Which action am I supposed to dispatch?"
By month 12, 41% of bugs were related to state management. By month 16, the team finally started discussing a rewrite. But by then, it would take three months of refactoring to recover.
Nobody did anything wrong. That's the most chilling part.
2. "Moving house" is not "convergence"
Earlier in that audit report, there was an even sharper sentence.
A project had just completed a "large-scale migration": the old structure moved wholesale into a new five-layer skeleton, with ESLint rules and boundary tests configured. The skeleton was directionally correct. But the report said plainly: "The migration only moved house; it didn't converge." 143 old components moved unchanged into the new structure, giant files were left as is, dual error systems, duplicated styles, and each piece of localStorage managing its own thing — all carried into the new home.
The most typical example was a 5647-line SessionWorkspace.vue. It simultaneously handled the assembly of 6 stores, per-thread runtime state bucketing, message projection merging, SSE stream orchestration, input queues, approvals, attachments, panel dragging, and ResizeObserver. Script was 3090 lines, styles 1730 lines, and it even @imported the entire global stylesheet into the component inside a scoped block.
It runs. It just became something no one dares to break apart.
The report also had a sentence more subtle than that 5647-line one: there was a real reverse import cycle in dependency direction. apis depended on identity, and identity depended back on apis. It hadn't blown the stack only because of "lazy calls inside function bodies" — a structural hazard, not a structural error.
"Not exploded yet" and "no problem" are two completely different things.
3. AI makes "creating new" cheaper, so "convergence" gets harder
In 2026, this matter has a new variable.
AI has stepped up the speed of component generation. But if you ask it to make a card, it will most likely generate a brand-new component rather than first asking "does the project already have one?" Its default goal is to satisfy the current page first, not to maintain the long-term order of the component system.
So there's a subtle reversal: before, you tended to reuse because writing a new set yourself wasn't cheap either. Now AI can "generate in place" a similar version in minutes, so the incentive to reuse weakens.
The result: for the same display card, you can find four or five versions in the project. Base component props keep piling up until nobody dares to touch them. Public components were meant for reuse, but they've become the easiest place to accumulate technical debt.
AI didn't create the problem. It just amplified "missing component governance" at a speed that you can no longer ignore.
4. I now judge whether code is "dangerous" by whether it's messy or not
After that "corporate procurement" night, I switched my standard.
Don't look at whether directories are tidy; look at one thing: does code that changes together for the same reason live together?
The validation for "checkout" only changes when the checkout requirement changes. So it should belong to "checkout", not live in a global utils next to a bunch of unrelated things. The old validator that quietly vetoed the new form was able to hide for two years precisely because it lived in "its proper place" rather than "the place checkout belongs to".
The difference between "moving house" and "convergence" is also here. Moving house is putting old things into a new cabinet. Convergence is deciding which things should no longer exist. 143 components moved unchanged into a five-layer skeleton; no matter how correct the skeleton, inside are still 143 old habits. Structure can be replaced overnight; habits will not.
As for that medical team where "every decision was correct", the way they survived was not "making all slices more standardized". It was spending three months re-answering the most plain question: who exactly should own this state?
Rot never happened because someone did something wrong at some moment. It's because no one ever stopped to make the decision to "take things back".