Most frontend codebases I’ve worked in didn’t break all at once. They got worse in ways nobody really noticed—or at least nobody said anything about.
It usually starts with something reasonable. A component needs to support a slightly different layout in one spot, so you add a prop. Someone else needs a variation in a different context, so they copy it and tweak it. Six months later you’ve got four versions of the same idea living in different folders, and the newest engineer on the team has to guess which one is “correct.”
I spent a long time thinking the fix was structure—better folder organization, stricter naming conventions, a proper design system. Those things help at the margins. But I don’t think they’re what actually moves the needle. The thing that actually matters is how easy your system makes it to do the right thing, and how much friction it puts in the way of the wrong thing.
Variation is the real problem, not complexity
When I look at a codebase that’s become painful to work in, it’s rarely just “too complex.” Complex code can still be understandable if it’s consistent. What makes things hard is when similar problems have been solved in five different ways and there’s no clear reason why.
You end up with three button components. Two patterns for fetching data. A handful of ways to handle form state, each one from a different era of the project’s history. None of those decisions were wrong individually—they probably made sense at the time. But together, they mean every new feature requires a detour: which approach are we supposed to use here? That detour is small, but it compounds. A system that forces that question constantly will feel slow and unpredictable to work in, even if none of the individual parts are complicated.
The cost of “flexible” components
There’s a well-intentioned instinct to make reusable components as flexible as possible—support every edge case, expose enough props that the component can handle anything you throw at it. In practice this tends to backfire.
A component that tries to do everything becomes hard to reason about. You can’t tell what it’ll render without tracing through conditional logic. It’s easy to misuse in subtle ways. And when you need to change it, you can’t be sure what you’re breaking.
I’ve had better results going the opposite direction: make components do less. Smaller, purpose-built components that are almost boring. If you need a variation, make a new component rather than adding another prop to the existing one. It feels inefficient at first, but boring components are predictable—and predictability is what actually lets you move fast.
Structure vs. patterns
A lot of teams invest in folder structure and file organization and then wonder why the codebase still feels chaotic. I think the disconnect is that structure is visible but patterns are what you actually live with day-to-day.
When a new person joins your project, they figure out the folder structure in an hour. What takes weeks is learning the unspoken answers to: where does state actually live? When something goes wrong in a form, where do I look? Are we using the router’s state or our own? How are API calls made?
If those answers are inconsistent—even slightly—the codebase will feel unpredictable in a way that’s hard to pin down. You open a file and it’s just… different from the one you were just in, for no apparent reason. The best codebases I’ve worked in had an almost boring consistency to them. That kind of predictability doesn’t come from good organization. It comes from teams that made decisions and then actually stuck with them.
Abstractions will drift
Every abstraction makes sense when you write it. The problem is that requirements keep coming. A hook that handled one clean use case gets an if-condition for a slight variation. Then another. Then a flag gets added “temporarily.” Six months later it’s handling three different scenarios in subtly inconsistent ways, and nobody wants to touch it because they’re not sure what they’d break.
The mistake is treating abstractions like permanent infrastructure. They’re more like working assumptions—useful until they’re not. The question worth asking isn’t “is this a good abstraction?” It’s “how easy will it be to replace this when it stops working?” If too many things depend on it, or its interface is too implicit, you’ll end up patching it forever instead of replacing it when you should.
The shortcuts add up
Most of the technical debt I’ve seen didn’t come from careless work. It came from reasonable decisions made under time pressure—handling an edge case inline instead of doing it properly, duplicating a component instead of refactoring the original, writing a quick fix with the intention to clean it up later.
The issue isn’t that these decisions are bad; they’re often the right call in the moment. The issue is that they’re invisible. The codebase doesn’t tell you they happened, and “later” almost never comes.
What separates maintainable systems from unmaintainable ones is how much the system itself pushes back on these things. Stricter types catch misuse at the edges. Clear module boundaries make it obvious when something’s being used in a way it wasn’t designed for. Consistent patterns mean a shortcut stands out instead of blending in. If taking a shortcut is just as easy as doing it right, people will take shortcuts—not because they’re lazy, but because they’re humans with deadlines.
This is a product problem, not just a codebase problem
The reason maintainability matters is pretty simple: a system that’s hard to change will produce a product that changes slowly. When every feature takes longer than it should because engineers are navigating around accumulated shortcuts, when bugs are hard to find because the relevant code is inconsistent, when teams start avoiding whole parts of the codebase because they’re not sure what they’ll break—that’s not just a developer experience problem. It limits what you’re able to build.
The teams I’ve seen ship the fastest weren’t always working with the best tech. They were working in systems that didn’t fight them.
A simple gut check
Pick a feature you’d expect to need to change in the next six months. Think through what that change would actually involve—which files you’d touch, what might break, how confident you’d feel. If you’re already anxious about a change you haven’t made yet, that’s telling you something.
Maintainability isn’t a score you can calculate. But it’s something you feel pretty quickly once you start working in a system. And the feeling is usually right.
The goal isn’t a codebase with no mess—that doesn’t exist. The goal is a codebase where the mess is predictable. Where the complexity that exists is there because it had to be, not because nobody stopped to clean it up. That gap is harder to maintain than any individual technical decision, and it matters more in the long run than most of the framework debates we spend our time on.