/** * Written by Tycho on June 5, 2025 * * @param tags mindset */
Build First, Abstract Later
In the previous part, we looked at the hidden cost of abstraction. How it often promises clarity, reusability, and elegance, but too easily delivers the opposite: indirection, fragility, and complexity.
Abstraction is not the enemy, though. In fact, good abstractions are essential to developing maintainable software.
But, abstractions should emerge from patterns, not predictions.
Modular != Abstract
In this second part, we’ll explore what that actually looks like: how to build modular, maintainable code without falling into abstraction hell.
Modular code isn’t necessarily abstract. It’s code with:
- clear boundaries
- stable contracts
- single responsibilities
Delay abstraction until it hurts
Delay abstraction until it gets in the way of progress, not before.
It’s much easier to grow into a well-fitting abstraction than to escape from a wrong one.
Resisting the urge to use abstraction gives you several key advantages:
-
It lets patterns prove themselves: The best abstractions don’t come from a whiteboard, they come from experience. From solving the same problem a few times, recognizing the true similarities, and then designing a better way.
-
It reduces rework and refactoring: Changing wrong abstractions often involves big refactoring of existing features, making it prone to breaking stuff in other places that used to work.
-
You stay agile: Concrete, specific code is easier to follow, test, and change. Avoiding abstraction until it’s needed will help your team move quickly and adapt confidently to evolving business needs.
-
Improves communication: Clear, localized logic is often easier to understand than generalized code with abstract names. When code is explicit and close to the problem it solves, onboarding gets faster and team discussions become clearer.
When in doubt, favor decoupling over early abstraction. Code that is well-separated but occasionally repetitive is far easier to evolve than code that’s DRY but tangled in abstractions that don’t reflect how the system actually works.
Characteristics of a good abstraction
Once a pattern has proven itself, and making changes become painful, it’s time to abstract. But carefully.
A good abstraction:
- is simple to use, and simple to ignore
- hides complexity without hiding intent
- matches how the domain actually works
- reduces duplication without reducing clarity
- simplifies usage without requiring deep internal knowledge
- remains stable over time, even as requirements shift
- makes change easier, not harder
Practice Thoughtful Abstraction
Above all, support a culture that values thoughtful abstraction. Teach engineers not just how to abstract, but when — and when not to. Push back on unnecessary generalization, and don’t equate modularity with complexity.
Here are some practical principles to follow:
1. Start by building concrete, readable code
Don’t shy away from duplication in the early stages. It’s okay to copy a few lines once or twice — often, that’s how real patterns begin to reveal themselves. Resist the pressure to abstract too early, especially in fast-moving or evolving domains where assumptions are likely to shift. Premature abstractions often introduce more confusion than they resolve.
2. Focus on clarity first
Code that’s easy to follow, even if slightly repetitive, is far more valuable than generalized code that hides intent. Encourage your team to value clear, well-factored logic — not just reuse for its own sake. When abstraction does happen, let it grow out of repetition and constraint, not speculation.
3. When it’s time to abstract, reach for the simplest tools
Favor composition over inheritance. Build functionality by combining small, well-defined pieces rather than relying on deep hierarchies. Prefer focused utility functions over abstract classes and interfaces. Group functionality based on what it actually does, rather than how it might be reused in the future.
TL;DR
Good software design isn’t about avoiding abstraction. It’s about applying it at the right time, for the right reasons.
Start with clear, concrete code. Let patterns emerge naturally before generalizing. Favor decoupling over early reuse, and optimize for clarity, not cleverness.
Modular code doesn’t have to be abstract. It just needs well-defined boundaries, stable contracts, and a clear purpose. When abstraction becomes necessary, keep it simple, grounded in reality, and easy to work with — or ignore.
Build first. Abstract later. Thoughtfully.