Engineering

What to do when the AI writes the wrong abstraction

Editorial · Reveneau · September 10, 2026

What to do when the AI writes the wrong abstraction

There is a type of problem in generated code that no check catches, no reviewer points out, and no dashboard shows. The code is correct. The tests pass. The structure looks like something an experienced engineer would write, because it has all the visible signs of good design: a shared base, a helper that removes repetition, a configuration object with reasonable defaults.

Six weeks later, every change in that area takes a day longer than it should, and nobody has connected the two facts.

This is the wrong abstraction, and it has become more common now that AI can write this kind of structure at almost no cost. A model asked to handle three similar cases will usually produce one clever thing that handles all three. That is not a bug in the model. It has read a large amount of code where unifying similar cases was exactly right, and unifying them is what a competent engineer often does. What the model does not have is your roadmap, so it cannot know that case two is about to get a completely different set of rules while cases one and three stay the same.

The cost is real and it arrives later, which makes it the hardest kind of problem to catch in review.

How to recognise one early

Three signals, in the order they usually appear.

A boolean parameter that changes behaviour. A function taking a flag that switches which branch runs is nearly always two functions that were merged because they looked similar at first. The flag is the exact place where the merge did not work. A tool can find this one automatically, and it is the earliest signal you get.

A change for one caller forces a change in shared code. This is the definitive test, and it is about consequences rather than appearance. If a product request affecting one consumer means editing a module four other consumers depend on, the abstraction is coupling things that need to change independently. Now every change carries the risk of the other three, and the review for a small feature has to cover code that has nothing to do with it.

Nobody can name it without saying "and". A component that validates and normalises and retries is three responsibilities in one file. Watch for names like handleData, processItem, or anything ending in Manager. The naming difficulty matters: a thing that is hard to name is usually a thing that has no single job.

There is a fourth signal that arrives too late to be useful as a warning but is worth recognising: people start working around it. Someone adds a second helper alongside the first because the first was too awkward to extend. Two overlapping helpers that do nearly the same thing means the first one failed, and nobody wanted to say so in a review.

Duplication is the smaller problem here

The instinct that produces wrong abstractions is a good one taken too early. Repetition genuinely is a maintenance cost. Three copies of the same logic means three places to fix a bug and three chances to miss one.

But the two mistakes behave in different ways. Duplication is visible, it is cheap to fix whenever you decide to, and its cost stays the same over time. You can see three similar functions and merge them in an afternoon once you know how they actually differ.

A wrong abstraction is invisible, its cost is spread across every future change, and it grows. An abstraction with two callers can be inlined in an hour. The same abstraction with fifteen callers is a piece of planned work with its own risk assessment. Nothing about it will look wrong at any point as it grows.

So the practical rule is to wait. Write the second case separately even when it looks like the first. Write the third separately too, if you can accept the repetition. By the third instance you can see which parts genuinely repeat and which parts only looked similar, and the abstraction you extract then is usually right, because it was derived from real cases rather than predicted from two.

That advice is old and most engineers know it. What changed is that the cost of generating the premature version dropped to almost nothing, so it now happens without anyone deciding to do it.

Whether to fix it at all

Not every wrong abstraction is worth removing, and removing them only to make the code cleaner wastes real time.

Ask one question: is it part of the code you are about to change? An abstraction that is badly designed but stable, sitting in a part of the system nobody has touched in a year, is costing you nothing this quarter. Rewriting it adds genuine risk for an improvement nobody will notice. Leave it and write down what you found.

Fix it when it blocks your work. If the next three tickets all change that module, the abstraction now adds cost to every item on the roadmap, and fixing it is straightforward work with a clear benefit. Explaining it this way also makes it easier to convince a product manager, because you are describing an obstacle to specific upcoming work rather than asking for time to make the code nicer.

How to take one out

The mechanics matter more than people expect, and getting the order wrong is what turns this into a multi-day debugging session.

Make the current behaviour detectable first. Before touching structure, assert what the code does today across the callers involved. Not what it should do: what it does. You are building a check that finds unintended change, and without it a change to the structure is a guess. The method is in adding evals to an existing codebase.

Inline it back to the call sites, and release that. Push the shared code back into each caller, with the branches resolved so each copy contains only the path it actually uses. The result will be uglier and longer, and it is correct at this stage. It is also a simple step-by-step change, which makes it easy to review, and your check will tell you if you made a mistake.

Then re-extract, only where repetition remains. With the code inlined you can see which parts are genuinely identical across callers and which only appeared to be. Usually less repeats than you assumed, and the shared piece that results is smaller and harder to get wrong.

The rule that keeps this safe is doing the two halves in separate changes. If you inline and re-extract in one pull request, any bug that appears could have come from either half, and you have lost the one advantage of doing it step by step.

Stopping it earlier

Most of this is preventable at the point the work is specified.

Describe the cases as separate requirements. An agent handed three requirements phrased as one parameterised family will build one parameterised thing, and it will be right to, given what it was told. Handed three separate requirements it will usually write three separate implementations, which is the state you want to be in when you decide what to unify. Getting the requirement itself precise enough to act on is covered in writing specs an agent can verify.

Say the constraint out loud in the instruction: write these separately until repetition is proven. It sounds too simple to matter, and it changes the output more than any other single line we have found.

And in review, ask the consequence question rather than the aesthetic one. Not "is this well structured", which starts an argument about personal taste that nobody wins. Ask: if one of these callers needs different behaviour next month, what has to change? If the answer is the shared component, you are looking at coupling, and it is cheaper to say so now than after the next four callers are added. Related reading on how review has to change for generated work is in why AI-generated code fails code review in a different way.

The part no tool can solve

Some of this is genuinely hard. Knowing which similarities are real and which are coincidental requires knowing where the product is going, and only the people on the team have that knowledge. No model has it, no linter encodes it, and no amount of test coverage substitutes for it.

This is one of the places where the human job is clearly defined. A check verifies a rule someone wrote down. Judging whether two things that look alike today will still look alike after the next two quarters is a judgment about the business, and it stays with the people who understand the business.

Thanks to the engineers who explained to us the abstractions they most regretted keeping. The pattern in almost every case was the same: it was correct when it was written, it was correct at the second caller, and nobody looked again at the fifth.

Repetition costs you a fixed amount forever. The wrong abstraction costs you more every time you change it.

Related guide: How to review AI-generated code.

Common questions

How do you tell if an abstraction is wrong rather than just unfamiliar?

Look at what happens when requirements change rather than at how the code reads. If a change requested by one caller forces you to modify shared code that several other callers depend on, the abstraction is coupling things that should change independently, and that is a fact about the structure rather than a matter of taste.

Why does generated code produce wrong abstractions so often?

Because a model asked to handle several similar cases will usually unify them, having seen a great deal of code where unification was correct. It has no access to your roadmap, so it cannot know which of those cases is about to become different, and premature unification is exactly the mistake that only becomes visible later.

Is duplicated code better than a wrong abstraction?

Usually yes, because duplication is cheap to fix and easy to see, while a wrong abstraction adds cost to every future change and hides that cost in unrelated tickets. Duplication is also reversible at any time, whereas removing an abstraction gets harder with every new caller that uses it.

What is the clearest early warning sign?

A boolean parameter that switches behaviour inside a shared function, which almost always means two different operations were merged because they looked alike at first. The parameter is the exact place where the merge did not work, and it is worth treating as a warning sign in review because a tool can find it automatically long before the coupling starts costing anyone time.

Should you fix a wrong abstraction as soon as you find it?

Only if it is part of the code you are about to change. An abstraction that is wrong but stable in a part of the system nobody touches is costing you nothing today, and rewriting it adds real risk for an improvement nobody will notice.

How do you remove an abstraction safely?

Inline it back into the call sites first and release that change on its own, then extract again only where the real code still shows genuine repetition. Doing both in one change means any resulting bug could have come from either half, which turns a simple step-by-step task into a debugging session.

How do you stop an agent from inventing abstractions in the first place?

Say in the specification that similar cases should be written separately until repetition is proven, and describe those cases as separate requirements rather than as one parameterised family. An agent handed three requirements phrased as a single family will correctly build a single parameterised thing, so the decision to unify them was made before any code exists.

What should a reviewer ask when a generated change adds a new shared component?

Ask what would happen if one consumer needed different behaviour next month, and ask whether the component can be named without using the word "and". If the answer to the first is that the shared code would have to change, and the answer to the second is no, the change is adding coupling rather than removing repetition.

Does having good tests make a wrong abstraction safe?

Tests make removing it safe, which is valuable, but they do nothing about the ongoing cost, because a wrong abstraction passes every test by definition. The tests protect you during the fix and tell you nothing about whether the fix is needed.

Why do wrong abstractions get worse over time rather than staying constant?

Because each new caller adds a dependency on the shared shape, so the cost of changing it rises with every use. An abstraction with two callers can be inlined in an afternoon, and the same abstraction with fifteen callers is a planned piece of work with its own risk.

How does this interact with taking over code you did not write?

You cannot tell a deliberate abstraction from an accidental one by reading, since both compile and both look intentional. Find out what the system must guarantee first, then judge structure against those guarantees rather than against how tidy the code looks.

Start a project