← Back to Blog
6 min read
Share

The Feature You Didn't Ask For

The Feature You Didn't Ask For

I asked for a function to fetch a user by ID. I got a class. It had a constructor with a timeoutMs parameter defaulted to 5000, an internal LRU cache with a configurable size, a retry policy with exponential backoff, and an interface — UserDataSource — so the underlying fetch strategy could be swapped later. It was well-typed. It had comments. It solved a problem I did not have, at a cost I didn't ask to pay.

I've stopped being surprised by this. It happens on nearly every non-trivial request, and I think it's worth naming directly, because "the code is over-engineered" undersells it — the code is fine, in isolation. The problem is that it answered a different, larger question than the one I asked.

Where the bias comes from

Models are trained on an enormous amount of code that's built to be reused — libraries, frameworks, published packages. That code should have a timeoutMs parameter, because it doesn't know its caller's tolerance for latency. It should be swappable behind an interface, because the library author has no idea what data source the fifty-thousandth downstream project will want. Generality is correct behavior for code whose caller is unknown.

Your one-off internal function fetching a user by ID has exactly one caller, and you're looking at it. It doesn't need to anticipate. But the model can't always tell the difference between "write me a general-purpose utility" and "write me the three lines this one call site needs" — both look, syntactically, like "write a function that does X." Left with that ambiguity, it reaches for the shape it's seen the most: the reusable, defensive, configurable one. That's the median answer, and the median answer for "how general should this be" is almost always more general than what a specific call site needs.

The cost isn't abstract

It's tempting to wave this off — extra code, so what, delete it if you don't need it. But every unused branch is a thing a future reader has to actively rule out, not just skip past. Open that UserDataSource interface six months from now, mid-debugging-session, and you have to determine — before you can move on — whether anything actually implements a second data source, whether the retry policy is load-bearing for some case you haven't hit yet, whether the cache is why a stale read you're chasing is stale. Every one of those questions costs real time, and every one of them has the same answer: no, it was speculative, nothing uses it. You just don't know that until you've checked.

This compounds badly with AI-assisted editing specifically, because the next assistant session — yours or a teammate's — reads that class and treats the interface, the retry policy, and the cache as signal. Structure in a codebase usually means something; that's the whole reason code review works. An unused abstraction that looks deliberate gets preserved, extended, and worked around instead of deleted, because nothing in the code itself says "this was never load-bearing." The single-caller function that grew a class doesn't just cost the reader who finds it once — it costs every future edit that has to work around a shape nobody actually needed.

The tells

A few patterns are close to diagnostic for "this was invented, not requested":

  • A "just in case" comment. If the justification for a piece of code is speculative future need rather than a current requirement, it's very likely dead weight the moment it's written.
  • A parameter that's always called with the same value. timeoutMs = 5000 with exactly one call site, always passing 5000 or nothing — that's not configuration, it's a constant wearing a costume.
  • An interface with one implementation. The entire point of an interface is to hide which of several implementations you're using. One implementation means there's nothing to hide from, yet.
  • Error handling for a case that can't occur. A try/catch around a call that only throws for a network condition your function already isn't reachable from — logging and re-throwing an error nothing upstream can act on differently.

None of these are wrong in the codebase where they earn their keep. They're wrong specifically when they show up unrequested, in response to a narrow ask, because "narrow ask" was never a signal the model was optimizing for the width of.

The fix is to specify the shape, not just the behavior

"Write a function to fetch a user by ID" describes the behavior and leaves the shape — one function versus a class, configurable versus fixed, defensive versus trusting — entirely to the model's judgment about what's typical. State the shape you actually want, the same way you'd state a failure mode or a storage choice when writing a spec:

A single async function, not a class. No caching, no retry, no configurable timeout — this is called once per request and a slow response should just be slow, not silently retried three times. Throw on not-found, don't return null.

That's not a longer request than the vague one. It's a more specific one, and specificity is the only lever you have against a model whose default, absent other signal, is the general-purpose answer.

The other half of the fix is reviewing for deletions, not just correctness. "Does this work" is the wrong question to stop at — the rate limiter class from a bug I wrote about elsewhere on this blog worked, cleanly, on every input I tried. "What's in here that I didn't ask for, and can I cut it" is the question that catches the parts that work fine and shouldn't exist. Every unrequested option should be treated as a bug until something in the actual codebase — not a hypothetical future need — proves it's load-bearing.

The smallest correct version usually isn't a draft

There's a reflex to treat the compact version as a first pass you'll flesh out later, and the four-hundred-line generated version as the "complete" one you're trimming down from. It's backwards more often than not. The three-line function that does exactly what the one call site needs isn't an unfinished version of the class — for a huge fraction of the code you write, it's the final answer, and the class was the thing that needed cutting down to reach it.

Related: The spec is the work now · Why your AI-generated tests don't catch anything · Common vibecoding mistakes

Get the good stuff

New tools and posts, occasionally. No spam.