The Chicken Problem
Code, like chicken, can be too DRY.
This isn't a radical idea. Joel Spolsky warned us about The Law of Leaky Abstractions. Sandi Metz taught us about The Wrong Abstraction. Yet we continue to reach for abstraction by default, treating duplication as the enemy when often abstraction is the more expensive choice.
After years of refactoring and re-architecting production code, I've learned that the cost of abstraction is often higher than we think, and the benefits are often lower than we hope.
Abstraction vs. Encapsulation
Before we dive into costs, let's clarify what we're talking about. These two concepts are often confused:
Encapsulation hides necessary complexity. It provides a simplified API with no instructions required, designed for the consumer. Think of a database connection pool or a well-designed React hook.
Abstraction solves for multiple uses. It provides an API designed for breadth with a learning curve and documentation requirements, designed for the implementer. Think of an ORM or a generic form builder.
The distinction matters because encapsulation is often necessary and valuable, while abstraction is where we tend to over-invest.
The Real Cost of Abstraction
1. Increased Complexity
The lifecycle of abstraction follows a predictable pattern:
- Necessary complexity is introduced (your business logic)
- Code is encapsulated for ease of use (good!)
- A unique requirement appears (uh oh)
- Abstraction is introduced to handle both cases (here we go)
What starts as a simple Dog
and Cat
class sharing a Pet
interface becomes Pet
with Walkable
and Swimmable
traits once you add Fish
. Each new requirement adds another layer of abstraction, another interface, another set of rules to remember.
This isn't free. Every abstraction is a concept that must be held in your head while working with the code. And humans are terrible at holding multiple complex thoughts simultaneously.
2. Barriers to Contribution
When evaluating who benefits from your code, consider this hierarchy:
- Customers - If it's not helping customers, that's a red flag
- Current Teammates - Can the on-call engineer fix a bug at 3am?
- Future Teammates - How does onboarding work? Can people get up to speed easily?
- Yourself - By far the least important
Abstractions often optimize for the author (saving keystrokes, feeling clever) at the expense of everyone else. The new engineer who needs to trace through three layers of inheritance to understand a simple feature. The on-call engineer who needs to understand your abstraction to fix a critical bug.
Humans are good at recognizing repetition and finding patterns. We're bad at holding simultaneous complex thoughts in our heads. Your abstraction forces everyone to do the latter.
3. Reduced Reliability
As the saying goes: "All non-trivial abstractions, to some degree, are leaky."
Your Walkable
interface will leak when you add a Snake
that moves but doesn't walk. Your generic form builder will leak when you need a form that behaves differently from all the others. Every abstraction promises to hide complexity but eventually reveals it at the worst possible time.
The Behavioral Economics of Bad Abstractions
Why do we keep making these mistakes?
Opportunity Cost
Every abstraction carries hidden costs:
- Education on how to use the abstraction
- Risk of future breakage and maintenance
- Increasing complexity to handle growing surface area
- Broader change impact (changing the abstraction affects everything)
- Time spent naming things (and naming is hard)
- Time not spent building value or cleaning up tech debt
Sunk Cost Fallacy
"We've already spent so much time maintaining this, it must be worth abstracting."
Or worse: "We've already spent so much time maintaining the abstraction, we can't get rid of it now."
Familiarity and Recency Bias
The abstraction's author is the least qualified person to evaluate its value. They understand it perfectly (they just wrote it!) and can't see how confusing it might be to others. They remember why every decision was made and assume others will too.
When Should You Abstract?
Ask yourself these questions:
- Does this solution solve for me or us? Be honest about who benefits.
- Will this abstraction bring tangible benefit to our customers? If not, reconsider.
- Can this problem be solved with tooling or documentation? Often a better choice.
- Are we just saving keystrokes? That's what snippets and editor macros are for.
- Are we optimizing for an unknown use case? YAGNI (You Aren't Gonna Need It).
Good use cases for abstraction are surprisingly rare:
- Platform code - UI component libraries, API clients, bootstrap utilities
- Truly generic systems with hard constraints - Data models, event systems, authentication
Everything else? Consider embracing the duplication. It's often cheaper than the abstraction.
The Paradox of DRY
Don't Repeat Yourself (DRY) has become dogma, but duplication is often the more maintainable choice. Three similar components are easier to understand and modify than one generic component with three configuration flags. When requirements diverge (and they will), you'll be grateful you didn't couple them together.
Remember: you can always abstract later when patterns become clear and stable. But it's much harder to remove an abstraction once it's woven throughout your codebase.
Code that's a little wet is better than code that's so DRY it's brittle.
Conclusion
Abstraction is a powerful tool, but it's an expensive one. Before reaching for it, consider whether the cost is worth the benefit. Often, the answer is no.
Embrace duplication when it makes code easier to understand. Optimize for your teammates' ability to work with your code, not your own comfort. And remember: the best code is not the code with the fewest keystrokes, but the code that solves problems for customers while remaining maintainable by your team.
The next time you're tempted to abstract, ask yourself: is this abstraction solving a real problem, or am I just afraid of a little repetition?
Sometimes, code needs to be a little wet to stay flexible.