Article · Blog
The abstraction you created to simplify things will be your next maintenance problem
Every abstraction is born from good intentions. The problem is that it ages without warning, and by the time you notice, it's already spread across ten places you no longer control. That's the cost nobody puts in the PR.
The abstraction you created to simplify things will be your next maintenance problem
In a financial app with a few million users, we built a networking wrapper. The idea was solid: centralize error handling, authentication headers, retry logic, and logging in one place. Any layer of the app would call that wrapper. No layer needed to know what was inside.
It worked for months.
Then someone needed to add a specific header for a payment route. We added a parameter. Then came conditional headers for another service. We added another parameter with a default value. Then someone needed exponential backoff retry, but only for critical endpoints. Another parameter. Another enum. Another case in the internal switch.
Two years in, the wrapper had 14 optional parameters, conditional logic scattered across four private methods, and not a single parameter with useful documentation. The abstraction that was supposed to hide complexity was generating new complexity.
Whenever someone on the team opened that file, they'd close it right away and go ask on Slack what to pass where.
The problem isn't the abstraction itself
Most debates about abstractions start from the wrong place. The conversation usually goes "you're over-abstracting" or "that abstraction is premature." Those statements aren't wrong, but they arrive too late and don't help you make any decision at the moment it actually matters.
The real problem isn't creating the abstraction. It's failing to recognize when it stopped serving its original purpose and turned into an accumulation of special cases.
Every abstraction has a tipping point. Before that point, it simplifies. After it, it disguises.
What makes this hard is that the tipping point is rarely a single event. It's a sequence of small, reasonable decisions, each one making sense in the context of whoever made it. No individual PR looks wrong. The problem is the accumulated result.
What happens in practice
You create a NetworkClient. It does one thing well.
Then comes the token authentication requirement. You add it. Then comes retry. You add it. Then comes structured logging. You add it. Each addition feels like the right place, because the client already knows about networking, authentication, headers.
But in the end, the NetworkClient knows about everything. And when something breaks, you don't know where. When you need to test, you don't know what to mock. When you need to swap the retry strategy, you're afraid of breaking the logging. When you need to add a new header, you're afraid of affecting routes that don't need it.
Abstractions that grow horizontally like this aren't abstractions. They're places where complexity got buried.
The accumulated convenience trap
There's a pattern I call Accumulated Convenience. It's when every addition to an existing abstraction feels simpler than creating a new one.
And often it is... in the short term.
Adding a parameter to an existing method takes five minutes. Creating a new protocol, defining the responsibility, adjusting injection points, updating tests, discussing the right name with the team... that takes hours.
So the obvious choice is convenience.
The problem is you make that choice fifteen times, and on the sixteenth you stop in front of the file and realize it's going to take a week just to understand what's happening in there before you can touch anything.
Abstractions that grow through convenience age poorly because they were never redesigned. They were only extended.
What works better
The idea that changed how I think about this was simple: an abstraction shouldn't grow, it should be replaced.
In practice that means when an abstraction needs a new parameter with conditional logic, that's the signal that the original design doesn't cover the new case. The right answer is rarely to extend. Almost always it's to create a new specialized abstraction and let the original do only what it did well.
In a more recent project, this translated into not having a single generic NetworkClient. We had a base client with common behavior, and domain-specific clients with their own rules, their own tests, and their own configuration surface. Each one was smaller and more testable than a single unified client would have been.
The cost was some duplication of common behaviors. It was worth it, because each part of the system could be understood and modified without fear of side effects.
The trade-off nobody says out loud
This approach has a real cost.
More specialized abstractions mean more files, more protocols, more injection points, more context that a new developer will need to absorb. A well-decomposed system can feel excessively fragmented to someone who arrives without the history.
And there's a balance point that isn't universal. It depends on team size, how frequently that area of the code changes, the expected lifespan of the product. There's no rule that works for everything.
What I've learned is that the cost of the wrong abstraction grows over time, and the cost of building the right abstraction is higher upfront and lower afterward. Most teams optimize for the short term because the pressure is immediate delivery. Then the debt keeps accumulating until the next refactor turns into a full sprint project.
What I would do differently
I would add one question to the code review process.
Not "is this code correct?", not "is this tested?", but: "is this change within the original scope of this abstraction, or is it expanding the scope?"
If it's expanding, that doesn't mean the change is wrong. It means it's worth discussing whether the abstraction needs to be rewritten or whether the new behavior should live somewhere else.
That question rarely comes up in PRs. It shows up years later, when someone needs to touch that file and realizes they can't do it without breaking something else.
What stays with you
Abstractions don't fail on the day they're created. They fail on the day you realize you can no longer modify them safely.
And that day almost always comes sooner than the team expected, because the abstraction grew through additions that nobody questioned individually.
The question worth asking before extending anything isn't "does this fit here?". It's "is this still the same thing?"
If the answer is uncertain... it probably isn't.