Audit BenchAi
← All posts

The Monolith-by-Default Problem in AI-Generated Codebases

·8 min read

Ask an AI coding assistant to add a feature and it will almost always take the path of least resistance: find the file that looks most related, and add the new logic there. That instinct is reasonable in isolation — it's the smallest possible diff, and it doesn't require the assistant to invent a new module boundary the human didn't ask for. Repeated across a hundred features, it produces a route handler, a service class, or a controller that has quietly grown into the de facto home for half the application's logic.

Why the assistant reaches for the nearest file

Proposing a new file, module, or service boundary carries a kind of risk an assistant is tuned to avoid: it might guess wrong about where the seam should be, second-guess a structure the developer already had in mind, or produce a larger, harder-to-review diff. Adding five more lines to an existing function is always defensible. Splitting that function into two responsibilities requires a judgment call about the codebase's future shape that the assistant has no reliable way to make from a single prompt. So it doesn't make the call — it defers, every time, to the smallest local change.

The 800-line route handler

This is the concrete shape the problem takes in practice: an endpoint that started as "fetch the user and return their profile" accretes validation, authorization checks, a notification side effect, an analytics event, a cache invalidation, and eventually a second unrelated feature that just needed something to run after the user was fetched. None of these additions was wrong on its own. The handler as a whole is now a single point of failure for testing, review, and change — one file that has to be understood in full before anyone can safely touch any part of it.

Why "it still works" hides the cost

A monolithic handler usually still passes its tests and behaves correctly, which is exactly why this doesn't get flagged as a bug. The cost shows up later and elsewhere: a change to the notification logic now requires understanding the authorization logic sitting next to it to be sure nothing gets broken; a new engineer can't safely modify one concern without reading the whole file; and the blast radius of a bug in any one responsibility now includes every other responsibility bundled into the same function. Correctness and maintainability are different axes, and AI-generated code is optimized far more reliably for the first than the second.

What good boundary-drawing looks like

The fix isn't to demand a new microservice for every feature — that overcorrects into its own kind of debt. It's to notice, at review time, when a file is accumulating unrelated responsibilities and to explicitly prompt for extraction: "pull the notification logic into its own function, called from here." An AI assistant is generally good at executing a well-specified extraction once asked; it just won't propose the extraction unprompted, because that requires foresight about the codebase that a single feature-request prompt doesn't give it.

Checklist for reviewing AI-generated feature additions

  1. Check the size and responsibility count of the file being modified, not just the diff — a five-line addition to an 800-line handler is a different review than a five-line addition to a 40-line one.
  2. When a handler starts doing more than one distinct thing (fetch and notify, validate and log), flag it for extraction rather than approving the incremental addition.
  3. Ask whether the new logic belongs to an existing responsibility or is a new one being bolted onto the nearest convenient file.
  4. Periodically review the largest files in the codebase specifically for accumulated, unrelated responsibilities, since this pattern never triggers a test failure on its own.

Read how to audit monorepos without losing signal →

See how to review generated code for production risk →