Skip to content
Essay · Engineering Taste

The Hidden Cost of Premature Abstraction

In algorithm design and in engineering generally, the most expensive bugs are not the ones we make, they are the abstractions we built before we understood the problem. This essay is about why most interview-grade solutions stop at the right level of abstraction, and why production code so often doesn't.

11 min readPublished · 21 May 2026

Every engineer learns the maxim "Don't Repeat Yourself" early, usually around the same time they learn to write a function. A few thousand lines later they discover the corollary the maxim hides: the cost of removing duplication is the cost of building an abstraction, and that cost is paid both when the abstraction is built and every time someone reads code through it. Premature abstraction has a specific signature in algorithm problems too, and learning to spot it is one of the more under-discussed skills in interview preparation.

Consider the array-rotation problem. Rotate an array of length n by k positions, in place, using O(1) extra memory. The classic solution is the reverse-twice technique: reverse the whole array, then reverse the first k elements, then reverse the remaining n - k. Three calls to a single primitive (array reversal) and the rotation falls out.

def rotate(a, k):
    k %= len(a)
    reverse(a, 0, len(a) - 1)
    reverse(a, 0, k - 1)
    reverse(a, k, len(a) - 1)

def reverse(a, lo, hi):
    while lo < hi:
        a[lo], a[hi] = a[hi], a[lo]
        lo += 1; hi -= 1

Now consider a junior engineer asked to refactor this for a codebase. They might extract the three-step rotation into a class called ArrayRotator. Then they might generalise the reversal to handle linked lists and tuples too, behind an abstract Reversible interface. Then they might decide that the rotation logic should live in a SequenceTransform module that also handles cyclic shifts of strings and rotations of polygon coordinates. Six months later there are 1,200 lines of code where the original six lines did the job. Nobody touches the abstraction without breaking something. The original technique (three sequential calls to a primitive) is buried inside a maze nobody fully understands.

Where the line is

This isn't a hypothetical. It's the modal failure of mid-career engineers building "framework code" in production systems. The abstraction urge is real and often correct, but it kicks in too early. The right time to abstract is when you have three distinct callers using nearly identical code, with a clear understanding of what variation to preserve. Two callers is too few. Three callers but where each one has a unique wrinkle is also too few. The cost of refactoring later is almost always less than the cost of building the wrong abstraction now.

The algorithm problems in this curriculum are excellent stress-tests for this judgment. A clean interview solution to "find the K-th largest" might use quickselect with a partition routine. A premature abstraction extracts the partition into an interface that also handles ranking, percentiles, and median-of-medians. The cleaner one runs faster, fits on a screen, and the next person reading it understands it in 30 seconds. The abstract one requires a guided tour.

Three signals that you're abstracting too early

The pattern recurs enough to have detectable warning signs. Three reliable ones:

  • You can't write down what varies. Good abstractions name what's variable across uses. If you find yourself saying "well it depends on the data" or "it varies by caller", you're hiding the variance instead of factoring it out. Better to inline the differences until the pattern is clear.
  • The first version of the abstraction needs configuration flags. "It's the same for both, except this case passes strict=True and that one passes strict=False" is a code smell. A boolean flag at the abstraction boundary almost always means there are two slightly different operations masquerading as one.
  • You're "abstracting in case we need it later." This is the most common failure mode. The third-call-defines-abstraction rule exists because two callers are not enough evidence that future callers will share the same shape. By the time the third comes, the pattern of variance is usually clear, and sometimes the third caller turns out to need something different enough that the right move is two parallel implementations, not one abstract one.

Algorithms versus engineering

The pattern shows up specifically in algorithm code because algorithms have known optimal forms. Two-pointer converging walks are five lines. Sliding windows are eight lines. Dijkstra's algorithm is fifteen lines. There's no productive way to "abstract" any of these without destroying their clarity. They are already at the right level of granularity. When senior engineers see a junior wrap quicksort in a class hierarchy, the discomfort isn't because hierarchies are bad, it's because they recognise that someone has reached for the wrong level of abstraction for the problem at hand.

The companion mistake (under-abstraction) is real but rarer. Under-abstraction looks like copying twenty lines of code into eight places. Most engineers spot this and refactor; few have ever caused real harm by under-abstracting because the next maintenance pass catches it. Over-abstraction is harder to undo. Once an interface ships, removing it requires the consent of everyone who depends on it. So the asymmetry biases toward delaying abstraction, not rushing it.

Production examples worth remembering

The InputStream hierarchy in Java is widely considered an over-abstraction. There are dozens of classes, each implementing a slightly different decoration of a basic byte stream. Reading a file through five layers of wrappers is the norm. The original goal (composable I/O) was good. The execution shipped before there was enough variance to justify the hierarchy. Twenty years later, the consensus is that java.nio.file.Files' utility methods are what most code should use, with the abstract hierarchy left for cases that demand it.

The Linux kernel's device-tree mechanism is sometimes cited as the opposite, a successful abstraction that emerged organically from three or four hardware platforms needing similar but slightly different configuration. The kernel community resisted abstracting until the third platform's needs became clear; then they extracted the device tree, tested it on the existing three, and shipped. New platforms now plug in cleanly. The cost was real maintenance overhead, but the abstraction earned its keep because variance was understood in advance.

What this means for interview code

When solving an interview problem, ship the simplest correct implementation first. Resist the urge to factor out helper methods for code that appears only once. Inline early iterations until you can see the variance. If the interviewer asks for a refactor, then refactor, but the original version that runs correctly with the right complexity is almost always more impressive than a version that's been over-engineered into a class hierarchy.

The same logic applies in production. A 30-line function that handles three known cases is easier to maintain than a 300-line system that handles "all possible cases" but only ever sees three of them. When the fourth case arrives, refactor the 30-line function, and chances are you'll discover that the fourth case is sufficiently different that it deserves its own 30-line function alongside the original.

The connection back to algorithms

Algorithm problems train this judgment well because their optimal forms are short and incompressible. There is no "abstraction over Kadane's algorithm" that improves it, the algorithm is the abstraction. Reading well-written algorithm code teaches engineers what a finished thought looks like at the right level of granularity. Reading over-engineered production code teaches them what it looks like when someone abstracted before they understood. Both lessons matter; the algorithm lessons just happen first, which is part of why they're worth investing in even if your day job is not algorithmically intensive.

The next time you find yourself reaching for an abstract base class to capture two similar code paths, ask: would I extract this if I had to solve it as a Leetcode hard problem on a whiteboard? If the answer is no, then probably this isn't the moment for an abstraction either. The shape of correct engineering looks the same whether the audience is a compiler or a colleague reading the code three years from now.