Engineering
AI Is Writing More Code Than Ever. So Why Is Software Getting Harder to Maintain?
Code generation got cheap. Understanding code did not. Here is why velocity from AI assistants often shows up later as review burden, duplication and technical debt, and what actually helps.
Every engineering team we talk to has the same two things happening at once.
The first is that they are shipping more code than they ever have. Autocomplete finishes the function. The agent writes the migration. A ticket that used to be an afternoon is now twenty minutes and a review.
The second is that the codebase feels worse. Not broken, exactly. Just heavier. Onboarding takes longer than it did. The same bug class keeps coming back in a new file. Nobody can quite say when it started.
Those two things are not a coincidence. They are the same event, seen from two ends.
The part that got cheaper, and the part that did not
Writing code was never the expensive part of software. Reading it was.
A line of code gets written once and then read, over and over, by people trying to change something near it. It gets read during review. It gets read at 2am by whoever is on call. It gets read eighteen months later by someone who was not there when the decision was made, and who has to work out from the code alone whether that early return is load bearing.
AI assistants made the writing step close to free. They did not make any of the reading steps cheaper. So the ratio moved. Teams that used to produce a certain amount of code per week now produce several times that, and the reading capacity on the other side is the same set of people it always was.
That is the whole paradox in one sentence. The bottleneck did not go away. It moved downstream, where it is harder to see.
Where the time actually goes now
When we look at what changed for teams after AI assistants became normal, the same four patterns come up.
Diffs got bigger, and review stayed human
A 40 line pull request gets read properly. A 900 line pull request gets skimmed, and everyone involved knows it.
This is not a character flaw. Attention is finite, and careful review is genuinely hard work. But generated code arrives in larger units than hand written code does, because there is no longer any cost pressure keeping the change small. The author did not type those 900 lines, so the author has not necessarily read all of them either.
What follows is predictable. Approval rates stay high, review depth quietly drops, and the reviewer becomes a rubber stamp on work nobody has fully read. The commit history looks healthy. The understanding behind it is thinner than it was two years ago.
The code is plausible, which is worse than being wrong
Wrong code is a gift. It fails the test, it throws in staging, it gets fixed before anyone builds on top of it.
Generated code is rarely wrong in that obliging way. It is usually plausible. It compiles, it passes the tests, it matches the shape of the surrounding file, and it is subtly not what your system does. It retries an operation your infrastructure already retries. It swallows an error your error handler was counting on. It uses a config value that exists, but which means something slightly different in this service than it did in the one the pattern came from.
Plausible code survives review, because review is largely a plausibility check. Then it becomes precedent, and the next generated change copies it.
The same helper, written five times
Ask an assistant to parse a duration string and it will write you a perfectly good duration parser. It will do this even though your repository already has one, in a shared package, with the timezone edge case fixed in it after an incident.
The model was not being careless. It could not see the helper. Its context was the file you had open and whatever fragments got retrieved for it, and your repository is a great deal larger than that.
Multiply this across a year and the effect is not one duplicate function. It is a slow loss of the property that makes a codebase maintainable: that there is one place to change a thing. Fixing the timezone bug now means finding five parsers, four of which nobody remembers writing.
Tests that agree with the bug
Generated tests are useful, and they have a specific failure mode worth naming. A test written from the implementation tends to assert what the implementation does, not what the behaviour is supposed to be.
If the function has an off by one error, a test generated from that function will assert the off by one result and pass forever. Coverage goes up. Confidence goes up. The bug is now protected by a test, which means the next person to fix it correctly will break the suite and assume they were wrong.
Why all of this comes back to context
Every one of those four patterns has the same root. The tool doing the writing had a narrow view of the system, and the process doing the checking had a narrow view too.
A human reviewer with five years on the codebase catches the duplicate parser instantly, because they remember the incident that produced the original. They catch the double retry, because they know what the infrastructure does. That knowledge is not in the diff. It is in the repository, in the ticket history, in the service boundaries, and in the changes that came before this one.
A reviewer who lacks that context, whether a person or a model, is limited to checking whether the diff makes sense on its own terms. Generated code almost always makes sense on its own terms. That is what it is optimised for.
So the useful question is not whether to review AI written code. Everyone reviews it. The question is whether the thing doing the reviewing knows more than the thing that did the writing. If it knows less, or the same, you are just checking plausibility twice.
What we built, and why it looks like this
This is the problem Codity was built around, and it is why the product is shaped the way it is.
Codity reads the whole repository rather than the diff. Every pull request is reviewed with full repository context, including linked tickets, service boundaries, and the changes that came before it. That is what makes it possible to say "there is already a duration parser in the shared package, and it handles a case yours does not" instead of "this function looks correct."
A few specifics, since the shape matters more than the pitch:
Retrieval is dynamic, not fixed. A one line config change and a refactor across nine files do not need the same amount of context. Using a fixed retrieval depth for both means over fetching on the small change and starving the large one, which is exactly when a review misses the definition it needed. We wrote about rebuilding this in more detail in our post on review quality.
Review runs as multiple agents, not one pass. Correctness, security and architecture are different reading tasks, and asking one pass to do all three produces a review that is shallow on each. Separating them is also what let us cut false positives hard, which matters more than it sounds: a reviewer that cries wolf gets muted, and a muted reviewer catches nothing.
Security is part of the same read. Vulnerability scanning, dependency issues and leaked secrets are checked in the pull request, not in a separate report that lands in a channel nobody opens.
Generated tests get run, not just written. Codity can open a pull request with tests in it, watch what CI does with them, and fix the ones that fail. A generated test that has never been executed is a guess. Our CI setup guide covers wiring that up on GitHub Actions and Azure Pipelines.
It works across GitHub, GitLab, Bitbucket and Azure DevOps, because the problem is not specific to one host.
What you can do regardless of tooling
Some of this is process, and you can start on it without buying anything.
Keep diffs small even when they are free to produce. The cost of a large change moved from writing it to reviewing it. Splitting a change is now the cheapest it has ever been, so split it.
Make the author read it first. The norm that quietly broke is that the person opening the pull request has read every line in it. Restore that norm explicitly and most of the review problem gets smaller.
Review tests against the spec, not the code. When a generated test comes in, ask what behaviour it is pinning down. If the answer is "whatever the function currently does," it is not a test, it is a snapshot.
Treat duplication as a first class review finding. A new function that already exists elsewhere should block a merge the same way a failing test does. This is the debt that compounds fastest and shows up latest.
Watch the trend, not the week. Review depth, time to first review, and how often the same area gets touched are more honest signals than lines shipped. Volume was always a bad proxy for progress. It is a worse one now.
The honest version
AI writing more code is not the problem, and going back is not the plan. The leverage is real and most teams are not giving it up.
The problem is that generation scaled and comprehension did not, and comprehension is what maintainability is made of. Any serious answer has to put context back on the reading side of the process, at the same scale the writing side now operates at.
That is the bet we are making. Read the whole repository, review every change against it, and catch the plausible mistakes before they become the pattern everything else copies.
If your team is shipping faster than it can read, that is the thing worth fixing. Book a demo and we will show you what it finds in your codebase.

