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.


