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.

By Vishy11 min readPublished · 21 May 2026Updated · 24 Sept 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 three-strikes table

The useful question is not "is this duplicated?" but "do I yet know what varies?". This table is how I decide, and the third column is the one that actually does the work.

OccurrencesWhat you knowRight moveCost of guessing wrong
OneNothing about variationWrite it inlineLow: deleting one function is easy
TwoOne axis of variation, possibly a coincidenceCopy it, and note the duplicationModerate: a wrong abstraction now shapes both call sites
ThreeThe real axis, and usually one surpriseExtract, parameterised by what actually differsLow: three examples pin the shape down
Three, but one differs structurallyThat two of them are the pattern and one is notExtract the two, leave the third aloneHigh if you force all three: the parameter list becomes a configuration language

The last row is the one people skip. Two similar things and one that merely rhymes is the most common shape in real code, and forcing all three through one interface is how a helper acquires its fourth boolean parameter.

A worked example, with a count

A service I worked on had three endpoints that each fetched a record, checked a permission and serialised a response. Someone extracted a GenericResourceHandler with hooks for fetching, checking and serialising. It was 180 lines replacing three functions of roughly 40 lines each, so on a pure line count it looked like a win of about 60 lines at the time it was written.

Eighteen months later that class had 340 lines, seven hook points, four boolean flags and served nine endpoints, two of which overrode enough hooks that they shared essentially nothing with the base but its inheritance edge. Tracing one request meant reading the subclass, the base, and two hook implementations in separate files, so a change that would have been a three-line edit in a 40-line function became a 45-minute exercise in confirming which of nine callers it would affect. The 60 lines saved were repaid with interest measured in comprehension time, which is the currency that actually runs out.

What would have worked: leave the three functions alone, and when the fourth arrived, extract only the genuinely identical part, which turned out to be the permission check, about nine lines. That abstraction would have been small, obviously correct and easy to delete.

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. Read the maximum subarray walkthrough and the thing that strikes you is how little of it can be factored out: two scalars, one recurrence, nothing left to name. 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.

Frequently asked questions

Isn't this just an argument against DRY?

No. DRY is about single sources of truth for knowledge, not about textual similarity. Two functions that look alike but encode different business rules are not a repetition, and merging them creates a false shared truth that the next requirement change will tear apart. DRY is well served by waiting until you know which knowledge is actually shared.

What if the duplication is in a place that is expensive to change later?

Then the calculation shifts, and that is the honest exception. Public APIs, wire formats and database schemas are costly to revise, so they deserve more design up front than an internal helper does. The rule of thumb scales with the cost of reversal: cheap to change, abstract late; expensive to change, think early.

How do I tell a good abstraction from a premature one in review?

Look at the parameter list. An abstraction derived from real cases tends to take one or two arguments that name a genuine axis of variation. One invented in advance accumulates flags, options and callbacks, because the author was guessing at variation rather than observing it. A boolean parameter is very often a merged pair of functions asking to be separated again.

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.

If you want to see the same discipline applied to algorithm choice rather than code structure, complexity is a contract makes the parallel argument about performance claims, and the dynamic programming module is a good place to watch small, incompressible solutions being derived rather than designed.