This article is published in English.
Why Frontend Abstractions Quietly Turn Into Technical Debt
Learn why premature frontend abstractions add hidden complexity and how to judge when shared components, hooks, or utilities are actually worth building.
There's a moment in nearly every frontend codebase when it quietly stops being an application and starts being a framework built to prop up an application.
You bring in a component library.
Then a design system arrives.
Then a state-management layer gets bolted on.
Then a data-fetching abstraction shows up.
Then a custom hook wraps that data-fetching abstraction.
Then someone writes a generic form component that takes a configuration object describing how forms should behave.
Before long, changing something as simple as a button means tracing through six files, three layers of abstraction, and a convention nobody can trace back to its inventor.
What's odd is that none of these individual choices seemed unreasonable at the time.
That's really the core issue with how frontend teams handle abstraction.
Most abstractions aren't inherently bad, and plenty of them genuinely help. The trouble is that frontend developers have gotten remarkably skilled at building abstractions before they've gathered enough proof that those abstractions are actually needed.
We're no longer just abstracting away existing complexity.
We're abstracting away the mere possibility that complexity might show up someday.
That habit produces a distinct kind of technical debt.
The Abstraction Usually Starts With Good Intentions
Picture three separate components, each responsible for fetching user data.
The first has a bit of duplicated loading logic.
The second repeats nearly the same pattern.
The third does something slightly different again.
Somebody spots the repetition and suggests:
"We should probably pull this into a shared abstraction."
That's a fair observation on its own.
So the team builds a custom hook:
const { data, loading, error } = useUserData(userId);
Nice and tidy.
Then a different component needs a small behavioral tweak.
Rather than hitting the API directly, the team bolts on another option:
useUserData(userId, {
includePermissions: true,
cache: true,
retry: 3,
});
A few months down the road, the hook isn't just fetching user records anymore.
It now handles caching, retries, permission checks, data transformations, error normalization, optimistic updates, and assorted feature-specific quirks.
The original duplication is gone.
But something else has crept in: a growing gap between the code and what it's actually doing.
Looking at a component no longer tells you where its data comes from.
You first have to understand the abstraction sitting in front of it.
That's the trade-off nobody brings up.
Abstraction doesn't eliminate complexity.
It relocates it.
Sometimes that relocation is genuinely worthwhile.
Other times you've just traded five lines of straightforward logic for three hundred lines of internal machinery.
Abstraction Has a Cost
Developers are taught early on to treat duplication as a liability.
Fair enough, it often is.
But duplication is far from the only kind of complexity.
Other forms include:
- indirection
- configuration
- implicit behavior
- generic APIs
- hidden dependencies
- conventions
- inheritance
- wrapper components
- debugging that only exists because of the abstraction itself
- cognitive load
Some duplication is genuinely cheaper than an elaborate abstraction.
Compare two approaches.
One repeats a small chunk of logic three times.
The other builds a generic utility with a dozen parameters, justified because three current use cases happen to overlap by roughly 60 percent.
The second option looks more polished, more "engineered."
It can also be considerably harder to maintain.
This is where frontend work regularly trips itself up.
Teams optimize for DRY code rather than for code that's easy to follow.
Those goals aren't interchangeable.
The Frontend Ecosystem Encourages This
Frontend development has a peculiar relationship with abstraction, largely because the whole ecosystem is stacked in layers by design.
A single modern application can easily pull in a rendering framework for building components, some kind of routing library, a client-side state manager, a separate tool for handling server-side state, plus a form library paired with its own validation package. On top of that sits a design system, a set of prebuilt UI components, a styling abstraction layered over plain CSS, a build tool orchestrating everything, and a testing framework watching over the whole setup.
Each of these solves a real problem on its own.
Things go sideways once the application starts stacking its own custom layers on top of all that.
Instead of using the framework directly, a developer reaches for an internal wrapper around it.
That wrapper, in turn, depends on yet another wrapper.
Before long, the team's day-to-day programming model barely resembles the underlying platform anymore.
This pattern shows up especially often in larger organizations.
A team might end up nesting something like:
<AppPage>
<DataBoundary>
<PermissionGate>
<FormContainer>
<EntityEditor />
</FormContainer>
</PermissionGate>
</DataBoundary>
</AppPage>
Each piece has a stated purpose.
Each layer has a documented reason for existing.
But the moment something breaks, a developer has to mentally rebuild the entire stack before even reaching the actual feature code.
That reconstruction isn't free, cognitively or in time spent.
Generic Components Are Often the Worst Offenders
One of the simplest paths to frontend complexity is designing a component meant to anticipate every future requirement.
It starts innocently:
<Button />
Then grows a bit:
<Button variant="primary" />
Then keeps expanding:
<Button
variant="primary"
size="large"
loading
icon={...}
permission="admin"
analyticsEvent="save"
confirm
/>
Eventually you're left with something that technically isn't a button anymore.
It's a miniature framework for rendering arbitrary actions.
The team feels productive, since new buttons can now be assembled through configuration instead of fresh implementation.
But configuration is still code, whether or not it looks like it.
In some ways it's worse, because configuration obscures the actual control flow.
Read twenty lines of straightforward code, and you can follow exactly what happens.
Read twenty lines of configuration, and you may need to dig into the component's implementation, trace through the configuration parser, figure out default values, and work out which options quietly interact with one another.
Explicit code has effectively been swapped out for a vocabulary.
That vocabulary can be genuinely powerful.
It can just as easily turn into a dialect nobody wants the job of maintaining.
The "Future-Proof" Trap
Future-proofing is usually the strongest justification anyone offers for adding an abstraction.
"We might need this later."
"There will probably be more variants down the road."
"This could get reused elsewhere in the app."
"Let's just build it generically from the start."
Occasionally that instinct is correct. Far more often, you simply don't have enough information yet to know.
The trouble is that any abstraction bakes in assumptions about the problem. Abstracting too early means locking in architectural choices before you actually understand what you're building. And once other parts of the codebase start depending on that abstraction, reversing course gets expensive fast.
This is exactly why early abstraction is riskier than it looks. Duplicated code is usually simple to refactor whenever you're ready. A flawed abstraction, on the other hand, tends to propagate outward.
Picture three components that look similar. You could leave the duplication alone for now. If a genuine shared pattern emerges over time, you extract it then. But if you jump straight to a generalized abstraction, every future use case gets bent to fit whatever assumptions you made at the start.
At that point, the abstraction stops being a convenience and starts being a constraint. The thing meant to eliminate repetition ends up making change harder than the duplication ever would have.
Good Abstractions Usually Come From Pain
The strongest abstractions tend not to be planned in advance. They get discovered.
A team implements the same behavior more than once. Eventually they notice which parts are truly identical and which only look alike on the surface. They come to understand what actually differs. Only then do they pull out the stable, shared core.
That produces a much sturdier foundation. You could describe the sequence like this:
Duplication leads to repetition, repetition leads to understanding, and understanding leads to abstraction.
Frontend teams, however, often follow a different path:
Possibility leads straight to abstraction, then to configuration, then to confusion.
The first approach costs more time up front. But across the life of the project it's typically faster overall, because the resulting abstraction reflects knowledge the team actually earned through experience.
Not All Duplication Should Be Removed
This is an uncomfortable truth for developers, since duplication tends to feel like a mistake by default. But duplication is sometimes the right call.
Say two components share nearly identical validation logic. If that logic is only five lines and each component answers to different business rules, leaving the code duplicated may be the smarter option.
Why? Because keeping them separate preserves local understanding. Someone working on one component later can adjust its behavior without worrying about accidentally breaking the other one.
Duplicated code implicitly says: these two things happen to resemble each other right now.
An abstraction says something much stronger: these two things are meant to be identical, and they should change together going forward.
That's a bigger claim to make. You should only reach for an abstraction when you genuinely believe that claim is true.
Abstractions Should Have a Small API
A useful practical check is this: how much do you actually need to learn before you can use this abstraction correctly?
If that list keeps growing, you're probably watching an abstraction turn into a framework.
A good abstraction conceals complexity. A bad one conceals decisions. Those sound similar, but they're not the same thing at all.
A well-designed API might look as simple as this:
const user = useUser(id);
A shakier one starts piling on flags and options, ending up something like this:
const user = useUser(id, {
cache: true,
normalize: true,
permissions: true,
optimistic: false,
retry: 3,
suspense: false,
transform: customTransform,
mode: "editor",
});
At some point the abstraction stops simplifying anything. It's just relocating the original problem into a configuration object. That shift is worth treating as a red flag.
Frontend Teams Need an Abstraction Budget
Teams already talk routinely about performance budgets, bundle-size budgets, error budgets, and infrastructure budgets. It's worth adding an abstraction budget to that list.
The goal isn't necessarily fewer abstractions in absolute terms. It's keeping the number of abstractions within what the team can comfortably hold in their heads.
Before adding a new one, it helps to ask a few questions.
How many times has this exact pattern shown up already? If the answer is once, don't abstract it yet. If it's twice, treat that as a reason for suspicion rather than action. Five occurrences starts to look like real evidence.
Next: do these things genuinely change for the same reasons? Looking alike isn't sufficient justification. Two components can appear nearly identical while evolving along completely different paths for different business reasons — in which case bundling them into one abstraction may be a mistake.
Finally: does this abstraction actually make the everyday case simpler? Not some hypothetical future case — the case people will hit constantly. If using the abstraction means re-reading its documentation every single time, you've likely traded one problem for a worse one.
The Best Frontend Code Is Often Boring
Software development has a curious status hierarchy built into it. A clever abstraction reads as more impressive than three plain components. A generic system feels more architecturally serious than one simple function. A reusable framework seems more professional than a bit of duplicated code.
But production systems don't reward code for looking sophisticated. They reward code that developers can safely modify.
The most valuable frontend code tends to be the boring kind. You open a component and can immediately see where its data originates. You can see exactly what fires when a user clicks something. You can edit the markup directly. You can trace how state flows. You can locate the API call without difficulty.
You shouldn't have to absorb a team's internal design philosophy just to fix a bug. That's not a sign of unsophisticated engineering — it's a sign of good engineering.
Abstraction Should Reduce Thinking, Not Increase It
Abstraction doesn't exist to make code look reusable. Its purpose is to make a system easier to reason about. That's the bar frontend teams should hold every abstraction to.
If an abstraction lets ten components share complex behavior without forcing every developer to understand how that behavior is implemented, it's earning its keep. If instead every developer has to learn a complicated API just to tweak one simple component, the abstraction is probably working against you.
This distinction matters because frontend work is already inherently difficult. Browsers are complicated. User interfaces are complicated. Keeping state in sync is complicated. Accessibility is complicated. Performance is complicated. There's no need to manufacture extra complexity on top of all that just so the architecture looks more sophisticated on paper.
The next stage of frontend development probably won't come from discovering yet another layer of abstraction. It will come from getting better at recognizing when not to build one.
The engineers who stand out won't necessarily be the ones who can design the most elaborately reusable component system. They'll be the ones who can look at a problem and correctly judge whether it calls for a system in the first place.
Sometimes the right abstraction is a single function. Sometimes it's a component. Sometimes it's just a well-named module. And sometimes it's five lines of duplicated code that anyone on the team can read and understand immediately.
Telling those situations apart is the actual skill worth developing.