/**
 * 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:

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:

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:

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.