Skip to content
Putting technology to work.
Insights to guide decisions and action.

Search articles

Inheriting "over-abstracted code" — The courage to revert to duplication

Table of contents · 6 items

"We only fixed the invoice display, but somehow the estimates screen broke." When taking over systems in custom development, we frequently encounter incidents where fixing one location breaks an unrelated area. Investigating these issues invariably uncovers the same pattern: shared functions or components consolidated early on because they looked similar ended up shouldering divergent requirements, so changes to either screen ripple across all of them. An abstraction created with good intentions ends up leaving the entire system in a state where touching anything breaks it.

This problem is captured brilliantly in Sandi Metz's well-known article, The Wrong Abstraction (sandimetz.com). Her core thesis is that "duplication is far cheaper than the wrong abstraction." Rather than forcing a premature abstraction, letting duplication exist for a while makes future maintenance substantially easier. In this article, we explore how to spot and safely untangle over-abstracted code inherited in custom development projects from a system development standpoint.

Why premature abstraction causes incidents

Code reuse and abstraction are fundamentally good practices. The problem arises when code that merely looks similar is bundled together before you can definitively say it is truly identical.

Suppose invoice and estimate rendering logic look identical at first. Because they appear the same, they are merged into a single function. Later, a tax exception is introduced only for invoices, while expiration dates apply only to estimates. The shared function begins accumulating conditional flags for invoice versus estimate contexts. Before long, one function shoulders both concerns, branches under if multiply, and modifying either scenario risks breaking the other. What was intended as "unification" becomes two distinct processes tangled into a single place.

DimensionSensible duplicationThe wrong abstraction
Scope of change impactIsolated to that locationRipples across all shared locations
Code appearanceMultiple similar routinesConsolidated and seemingly clean
Conditional branching (flags)LowContinues to grow per use case
Ease of handoverLocalized and readableCannot be touched without understanding the whole

The treacherous aspect of the wrong abstraction is that it looks clean on the surface. Because duplication has disappeared and the code is consolidated, people mistake it for good design. In reality, things that ought to be distinct have become tightly coupled. Without a record of why that architecture was chosen, successors inherit code where "touching it means you don't know what might break." Documenting architectural rationales was covered in our article on Architecture Decision Records (ADRs); without this context, untangling coupled abstractions becomes significantly more difficult.

Signs that identify the wrong abstraction

You can recognize whether inherited code suffers from the wrong abstraction by watching for the following signals:

First, flags and parameters to toggle behavior multiply continuously. When conditions like type or isInvoice line up at the beginning of a function and callers must supply them, it is a telltale sign that a single abstraction is serving multiple distinct purposes.

Second, modifying a shared component breaks tests on unrelated screens. When the blast radius of a change is unpredictable, it proves that distinct concerns have been artificially merged.

Third, no one can explain what a specific branch exists for. This indicates that abstraction is being preserved out of inertia long after the initial assumption of similarity dissolved.

When these symptoms align, rather than forcing an even cleverer abstraction, returning to duplication is often the safer path. Sandi Metz's recommendation to accept duplication and move forward rather than clinging to the wrong abstraction reflects this exact judgment.

A safe workflow for returning to duplication

When untangling this in an inherited system, the general workflow is as follows:

First, duplicate the coupled shared component across each distinct use case. Make two identical copies of the code—one for invoices and one for estimates—and point each caller to its respective copy. While total lines of code increase initially, the crucial win is that mutual interference is severed.

Next, strip unused branching from each separated copy. Remove if intended for estimates from the invoice copy, and remove invoice-specific branches from the estimate copy. Each implementation immediately becomes straightforward code tailored solely to its own domain, dramatically improving readability.

Finally, re-evaluate and extract only what is genuinely shared. Instead of consolidating everything, share only the elements that are demonstrably identical and will not diverge in the future. Following this sequence naturally aligns with the mindset of anchoring external API calls in robust foundations—sharing only truly common infrastructure, as discussed in our article on building resilient HTTP clients with the Fetch API.

Pitfalls When Integrating Agents in Custom Development

In an enterprise system that GleamHub took over for maintenance (client name withheld), a single shared component handled print output across invoices, estimates, delivery slips, and receipts. Internally, it was packed with conditional branches. When asked to adjust copy on receipts, the change accidentally broke the invoice layout. The predecessor had justified it by saying "shared code is efficient," but in practice, four distinct documents were intertwined in one place, leaving no one with a clear grasp of the whole picture.

We copied this shared component into four separate implementations and removed the branches belonging to other documents from each. Although the total volume of code temporarily increased, modifying receipts no longer impacted invoices, and each document logic became straightforward and dedicated. We then re-extracted only the truly common pieces, such as rendering header logos. What worked was not adding clever abstractions, but severing inappropriate coupling before consolidating genuinely shared logic.

The most valuable lesson from this project was not viewing duplication as inherently evil and abstraction as inherently good. Looking strictly at line counts makes duplication look wasteful, but from a maintenance standpoint, premature abstraction is far more expensive. Much like deciding whether to centralize repositories and dependencies, deciding what to share and what to isolate should be guided by maintainability. The decision-making criteria discussed in our article on managing dependencies in monorepos apply just as directly to code abstraction.

Where to begin

Inherited systems become safer when you abandon the assumption that "shared code equals good code" and evaluate quality based on whether the blast radius of changes is predictable. If you encounter shared components where flags keep multiplying and fixing one area breaks another, they may not be candidates for clever refactoring—they may be prime candidates for returning to duplication.

As a first step, identify one shared component in your current codebase that has numerous mode-switching flags, count the screens using it, and tally recent unexpected regressions that occurred there. Finding a component with an unpredictable impact radius pinpoints your top candidate for untangling.

If changing one area breaks another in an inherited system, if regressions occur with every release despite shared components, or if you need guidance on what to share versus what to decouple, please reach out via GleamHub's contact form. We will assess the abstraction health of your codebase and collaborate on an architectural plan to safely decouple problematic dependencies and transition toward a predictable structure.

Sources

Share this articleXFacebook
Kakeru Suzuki

Fascinated by the possibilities of technology, has had a deep interest in programming and digital art since student days

Turn this article's theme into your company's next step

Concrete steps forward for your organization.

We organize your desired architecture, legacy systems, and operational requirements to formulate your next steps toward execution.

  • Desired architecture
  • Integration with existing environments
  • Operational requirements
Consult on development & operations initiatives

You can consult with us from the initial conceptual stage. Details from this article will be carried over to the inquiry form.

Receive the latest articles by email