Optimize for Understanding, Not Reuse
Why shared abstractions and premature frameworks often increase complexity - and when a little duplication might be the better design choice.
Code reuse is usually treated as an unquestionable good. When two classes look similar, we extract a shared abstraction. When state appears in several places, we introduce a state-management framework.
The intention is reasonable: reduce duplication and make future changes easier. But, unfortunately, reuse is not free. Shared abstractions create coupling, and additional layers of them make the system harder to understand.
That’s why, sometimes, a little duplication is the simpler option.
Similar Does Not Mean the Same
Two classes may contain similar code while representing different business concepts. Extracting their common behaviour assumes they will continue evolving in the same direction.
When that assumption turns out to be wrong, the abstraction starts accumulating:
- Configuration flags for individual consumers.
- Generic types supporting slightly different models.
- Many conditions for special cases.
- Base-class methods used by only one implementation.
The duplicated code disappears, but the differences do not. They become hidden inside configuration and conditional logic.
One generic class rarely remains alone. It starts depending on generic factories, strategies, adapters and base types. Understanding a single operation may require jumping through several files and tracing type parameters across multiple inheritance or composition layers.
This makes the codebase harder to navigate and maintain. A developer trying to change one concrete behaviour must first understand a framework designed to support every possible behaviour.
At some point, two explicit repositories might have been easier to find, understand and modify.
Duplication Can Preserve Independence
Duplication has a visible cost: a change may need to be made in multiple places.
Abstraction has a less visible cost: previously independent components must now evolve together. A change to a shared generic class can also affect consumers that the developer did not know existed, increasing the scope and risk of otherwise local modifications.
A small amount of local duplication may be reasonable when:
- The repeated code is short and straightforward.
- The components belong to different business domains.
- Their future evolution is uncertain.
- Updating two copies is cheaper than maintaining a generic solution.
Duplication should not be ignored, but neither should it be removed automatically. Its cost should be compared with the coupling and cognitive overhead introduced by the abstraction.
The NgRx Example
NgRx is a powerful tool. For applications with complex shared state, many sources of change and non-trivial side effects, its explicit model can be valuable.
But it is sometimes introduced before the application has those problems.
A simple flow:
Component → Service → API
becomes:
Component → Action → Effect → Service → API
↓
Reducer → Selector → Component
The second model is not inherently wrong. It is simply more expensive.
Developers must navigate more files, understand more concepts and follow data through several layers. A small feature may require actions, reducers, selectors, effects and additional tests even when its behaviour is straightforward.
The question is not whether NgRx is a good framework. The question is whether it solves enough real complexity to justify the cognitive load.
The same applies to generic base classes, universal repository patterns and internal frameworks.
Extract Abstractions When They Become Clear
Before introducing an abstraction, ask:
- Do these cases represent the same concept, or do they only look similar?
- Will they realistically evolve together?
- Does the abstraction reduce the number of concepts and files developers must understand?
- Can its behaviour be explained without listing exceptions and configuration flags?
Waiting for several real use cases usually makes the correct abstraction easier to identify. You can then extract behaviour that is genuinely shared instead of designing for hypothetical requirements.
Conclusion
The goal of software design is not to maximise code reuse. It is to make the system understandable and safe to change.
Sometimes that requires a shared abstraction or a framework such as NgRx. Sometimes it means accepting two similar classes because keeping them explicit and independent is more valuable than eliminating a few repeated lines.
Simple code is not code without patterns. It is code with no more concepts than the problem actually requires.