Why Letting AI Drive Produces Worse Code

The productive AI-generated code I've seen in the last year is genuinely impressive. It compiles. It's tested. It's often stylistically clean. If you were reading it in isolation, you'd think it was written by a competent developer.

The problematic AI-generated code I've seen in the same year has the same properties. It compiles. It's tested. It's stylistically clean. That's the whole problem.

Bad AI-generated code doesn't look bad. It looks fine. It fails in specific ways that don't trip the alarms most teams use to catch bad code, and the failures compound over time in ways that get expensive to unwind. This piece is about what those failure modes are, why they happen, and what senior judgment is actually doing when it's in the loop.

The three failure modes

Almost every bad AI-generated code decision I've watched fits into one of three shapes.

Speculative abstraction. The AI extracts a class, an interface, a helper, a domain model, "in case it gets reused." No second caller exists. The extraction is defended on the grounds of clean architecture principles. What actually results is a codebase with more classes than callers, more indirection than the domain warrants, and more surface area to maintain than the actual complexity requires. Domain models without callers are debt. AI is systematically biased to create them.

Concretely: I recently watched an AI first-pass extraction propose a new domain model based on the argument that "four generation services duplicate this query pattern." On re-grep, only one of the four services was actually doing the query the extraction described. The other three were doing a related-but-different pattern that happened to share a keyword. Conflating them into one domain model would have made the extracted class a god-object serving four different concerns. The pattern-matching that suggested the extraction was superficial. Verifying the claim required manual inspection the AI didn't do on its own.

First-good-enough acceptance. The AI produces a solution that works. Tests pass. It goes in. The problem is that the solution is the middle-of-the-distribution answer, not the right one. Passing tests is not the same as being well-shaped. Passing tests just means the behavior is preserved, which is a lower bar than "the code is designed well."

Concretely again: on the same refactor, the AI's first-pass implementation extracted eight rule classes with an #items method each, and every method rebuilt the item structure inline. Tests passed. Lint clean. But every keyword argument being passed to the item constructor was derivable from self, a textbook data-clump smell. The pattern the code was asking for was Template Method: base class owns construction, subclasses declare properties via hook methods. The AI's first pass didn't see it. It saw "extract methods; make each a class." Which is right, structurally, but stops one layer short of the pattern the code actually wanted. Catching that required me to read the extracted classes again after they were "done" and push back on what the AI thought was a completed refactor.

Pattern-matched architecture over local context. The AI recognizes a familiar problem shape and applies a familiar solution, even when the local context argues for something simpler. "This looks like a service that needs a strategy pattern," even when the actual situation has one strategy and no realistic path to a second. The pattern-matching is wide. It's also blind to why patterns exist in the first place, which is to solve real problems, not to be applied preemptively.

These failure modes have the same root cause. The AI is trained on a corpus of code. That corpus rewards clean-looking abstractions, familiar patterns, and comprehensive coverage. The AI reproduces those defaults. Whether they fit the specific problem in front of you is not a question the AI can answer on its own.

What the empirical data says

GitClear ran a study analyzing about 211 million lines of code change data from 2020 to 2024, comparing AI-heavy codebases against pre-AI baselines. The findings match what I've been watching happen up close:

  • Code churn (revisions within two weeks of the initial commit) grew from 3.1% in 2020 to 5.7% in 2024, with AI-heavy projects seeing about 39% higher churn.
  • Duplicated code blocks rose roughly eightfold in 2024.
  • AI-authored PRs carried about 1.7x more issues per PR (roughly 10.8 versus 6.4) than non-AI PRs in the same codebases.
  • Estimated tech debt increased somewhere in the 30-41% range post-adoption.

None of these numbers say AI is bad. They say unsupervised AI amplifies whatever pattern is already there, and the default pattern for most teams is subtle wrongness. More code, faster, more of it slightly off, more of it copy-pasted rather than reused. Absent a discipline that catches the failure modes, the AI's throughput becomes the codebase's drift rate.

The full GitClear report is public if you want the methodology and the raw numbers. Their "AI Copilot Code Quality: 2025 Data Suggests 4x Growth in Code Clones" writeup is the primary document.

What senior intuition is doing

The person in the loop who catches these failure modes is doing something specific, and it's worth naming, because most conversations about "code review of AI output" gloss right past it.

The senior isn't checking that the code is correct. Tests already check that. The senior is checking that the code is shaped right for what it needs to be. That's a different skill. It requires having enough exposure to codebases-in-decay to recognize the shape of decay early. It requires being able to look at a proposed abstraction and say "this hasn't earned its place yet." It requires being able to look at a first-good-enough solution and say "the shape is still wrong, even though the tests pass."

That's not code review. That's design judgment. It's the thing the AI doesn't have.

The most concrete version of what senior judgment is doing:

  • Recognizing when a proposed abstraction has no second caller and pushing back.
  • Recognizing when a solution passes tests but leaves a smell (parameter passing where instance methods would do, imperative construction where declarative would read better, names that leak presentation into the domain layer) and iterating.
  • Recognizing when a pattern is being applied for pattern's sake instead of for the situation's actual needs.
  • Refusing to conflate adjacent-but-different concerns just because the AI's pattern-matching links them.
  • Distinguishing "this could theoretically get reused" from "this is already duplicated in the code AND has a real second caller coming."

Every one of those is a small judgment call. Each one is worth a small amount, individually. Miss enough of them and the codebase drifts. Because the AI's throughput is so high, the drift can happen fast.

Being senior doesn't eliminate this. It reduces it. Even careful, experienced reviewers let things through, some of the time the reviewer notices a smell and rationalizes their way past it because fixing it would slow them down, some of the time the smell is small enough to miss on a busy read. The senior instinct is calibrated on the large-shape stuff, god-object services, misplaced abstraction layers, wrong-named domain models. It's less reliable on the small-shape stuff, a slightly-too-long method, a variable name doing too much semantic work, a curve-fitted test. The small stuff accumulates whether the senior catches it or not. But the reduction rate, even a modest reduction rate, compounds over months and years, and that compounding is the whole difference between a codebase that stays healthy and one that gets rewritten in two years.

The mid-level failure mode, concretely

The place I've watched this go pathological is with mid-level engineers who use AI heavily and don't yet have the senior intuition to catch the wrong shape early. On a recent project, I watched a capable engineer go in circles for weeks. Their pattern:

  1. Hand a problem to the AI.
  2. Get a solution.
  3. Apply it. Tests pass.
  4. Ship.
  5. A day later, an edge-case bug surfaces from the subtle design flaw the AI's solution embedded.
  6. Hand the bug to the AI.
  7. Get a patch. Tests pass.
  8. Apply. Ship.
  9. A day later, a different subtle problem surfaces from the patch.
  10. Repeat.

On paper, this engineer was productive. In practice, they were writing code that needed rewriting almost as fast as they wrote it. The 39% churn number GitClear measured has a concrete shape to it, and this engineer's commit history was exactly that shape.

The engineer wasn't careless. They were doing what the AI encouraged them to do: accept the first solution, ship it, move on. They didn't have the calibrated intuition that would have said "wait, this solution has a shape problem that will bite us in production," so they never asked for a second option. Every solution looked fine when they applied it. The subtle wrongness only revealed itself after the fact.

A senior looking at the same starting problems would have caught the flaws before the first solutions shipped. Or would have pushed back on the AI's first pass and asked for alternatives. The mid-level engineer didn't know to do either, and the AI didn't do it for them.

This is the pattern managers should be looking for. It doesn't show up in "is code shipping" metrics. It shows up in "how often is code shipping that needs to be revised within a week" metrics. If that number is climbing, you have a driver problem, not a productivity problem.

The team-level failure

The most common failure I see in teams that have adopted AI heavily: they haven't articulated who's supposed to be doing the senior judgment step, and by default no one is.

A junior developer using AI is likely to accept the first-pass output. Nothing in their experience trains them to spot the "shape is wrong" smell yet. That's fine, if they're being paired with a senior. It's a problem if they aren't.

A mid-level developer using AI often produces code that reads better than they used to write solo, which reads to management like a productivity win. But if you look at the shape of what's landing, the same three failure modes are in the diffs. The mid-level developer doesn't catch them because their own calibration isn't yet at the level where they would.

A senior developer using AI can produce excellent work, but only if they're using the workflow that requires them to critique the AI's output. If they're using the AI passively, accepting completions, taking first-pass outputs to production, they've effectively demoted themselves. Their senior judgment doesn't help if it isn't in the loop.

The team-level pattern is that AI adoption is often unstructured. Nobody says explicitly "AI output requires a senior review." Nobody says "AI output should always come with the alternatives the AI considered." Nobody says "if the AI's first pass looks fine, that's your signal to re-read it, not your signal to ship." So the drift happens.

What managing this actually looks like

The intervention that catches AI drift is human review at the "is this well-shaped" level. That's not the same as code review at the "does this work" level, and most existing review processes are calibrated to the second, not the first.

Concrete things that seem to help:

Require AI-generated code to come with the alternatives that were considered. If a developer used AI to write a solution, ask them to also show the two other solutions they didn't pick and why. This is the discipline the AI needs to be pushed toward anyway, and it produces evidence that the design decision was made deliberately, not by accepting the first thing that came out.

Explicitly name "shape" as a review dimension. Reviewers should feel empowered to say "this works but the shape is wrong" and have that be a valid review comment. Right now, in a lot of teams, that comment reads as bikeshedding. It shouldn't. The shape of the code is the thing that will still be right (or wrong) in three years.

Watch for the drift signals. Rising abstraction count without corresponding domain complexity is a signal. New classes that only have one caller are a signal. Solutions that pattern-match to famous designs without local justification are a signal. Any of these individually is fine. All of them accumulating is a codebase headed toward a rewrite in two years.

Track code churn as a leading indicator. If your team's commit history shows a rising rate of revisions within a week of initial commit, that's the GitClear number reflected in your own data. Watch it monthly. If it's climbing while AI adoption is climbing, the two are almost certainly connected.

Give seniors permission to slow down AI-heavy juniors. If a junior is producing three PRs a day of AI-generated code, and a senior can only meaningfully review one, the answer is not "let two ship unreviewed." The answer is that the junior needs to slow down. Faster production of unreviewed code is negative value, not positive value.

Make pair-programming an explicit senior/mid-level pairing pattern. Not every mid-level engineer needs to sit with a senior all the time, but for the specific work that requires the senior instinct to shape (a new domain, an unfamiliar codebase, a refactor of load-bearing code), the pairing is what develops the intuition. The AI is not a substitute for that pairing. It is a substitute for typing.

Explicitly reward "the refactor after the refactor." When a mid-level engineer's AI-assisted work needs a senior to come back and reshape it, that reshape is high-value work. Team norms should treat it that way, not as "someone else fixing your work." Otherwise mid-level engineers hide the reshape need, and the drift accumulates unnoticed.

The metrics to watch

If you're a manager trying to figure out whether AI adoption at your company is going in the healthy direction or the drift direction, the metrics that are actually diagnostic:

Code churn rate (commits revised within two weeks of first commit). If it's climbing while AI usage climbs, you have a driver problem. The GitClear numbers give you a benchmark: pre-AI, this was around 3-4%. Above 5-6% is concerning.

PR review comments per PR. If they're rising, your reviewers are catching things, good. If they're stable while the AI-authored PR count is rising, your reviewers are letting more through.

Time-from-first-commit-to-final-shape. Not time to ship. Time to the version of the code that will still be there in six months. If code is shipping fast but getting reshaped repeatedly, the reshaping is where the real cost is.

Duplicated code blocks per feature. If features are adding more duplication than they're removing, the AI's default of regenerating rather than reusing is winning. This is fixable, but only if you're measuring it.

New abstraction count vs. domain complexity growth. If your app is adding domain models faster than it's adding domain, the abstractions are speculative. Prune them or the codebase becomes a maze.

These aren't hard metrics to compute. They're just metrics most teams don't currently look at. The teams that adopt AI without watching them are the teams whose codebases quietly rot.

Positions that aren't defensible

"AI code needs less review because it passes tests" is the position that produces the drift. Passing tests is a floor, not a ceiling. Refusing to raise the ceiling because tests pass is how codebases quietly rot.

"Senior engineers shouldn't have to review AI output" is also indefensible. That's what the senior judgment is for. Skipping that step doesn't save time. It shifts the time to future engineers who have to unwind the drift.

"We can't tell if code is AI-generated so we can't review it differently" is technically true but misses the point. The review discipline should apply to all code. The AI output happens to fail the discipline more often, but that's the review's job to catch, not to know the origin.

"Our team is small and can't afford senior review on every PR" is a real constraint, but the answer isn't "skip review." The answer is to accept that the codebase will drift proportionally to the review skipped, and to plan for the reshaping work that will need to happen later. Every skipped review is deferred debt, not saved time.

The bigger point

AI is a real amplifier. That's not in doubt. The question is what it amplifies.

If it's amplifying the throughput of a senior who's making design decisions well, you get transformative productivity. If it's amplifying the throughput of unreviewed decisions, you get transformative drift. The tool is the same. The presence or absence of judgment in the loop is the whole thing.

Most teams adopting AI right now are optimizing for throughput without asking about the shape of what's being produced. That's a bet. If it works out, they save a lot of engineering hours. If it doesn't, they end up with codebases that look like they were built by twelve people over three years, when in fact they were built by three people over six months. That's a specific kind of technical debt, and the interest rate on it isn't obvious until much later.

The intervention isn't hard. It's just easy to skip.


If your team is figuring out what "good AI adoption" actually looks like in practice, that's a conversation Rock Agile has become deep in. If it'd help to compare notes, get in touch.

Previous
Previous

Ten Strategies for Saying No

Next
Next

Sprockets and Import Maps, Side by Side: A Practical Coexistence for Legacy Rails Apps