API Design in the Era of LLMs

Why interface stability has quietly become a correctness property

R
machine-learning
essay
Author

Imad El Badisy

Published

August 27, 2026

This is an opinion piece, and it’s one I’ve held for a while, building funcml, but the last couple of years of coding assistants have sharpened it into something more specific: keeping a public interface stable isn’t just being polite to your users anymore. It’s starting to be part of whether your software is correct.

The claim

A library has two audiences now. There’s the person who reads today’s documentation and writes code against the current version - that’s the old audience, still there. And then there’s every language model trained on the public record of how your library has been used across all its past versions, which is new and honestly enormous. That second audience doesn’t read your changelog. It has no way to tell a current idiom from one you retired four years ago. It just reproduces whatever pattern showed up most in its training data, and it does that with total confidence, no version awareness at all.

If your API hasn’t changed much, both audiences land on the same answer. If it has churned, the model turns into something that hands out code that no longer runs - or, worse, code that runs but doesn’t mean what it used to.

Interface stability used to be about usability. Now it’s closer to a correctness guarantee, because a lot of the code written against your library is going to be generated by something whose knowledge is an average over your whole history, not a snapshot of where you are today.

What actually changed

A few things happened around the same time.

People stopped searching and started generating. A few years ago, if you got stuck on an API you searched, skimmed a few results, checked the dates, and adjusted. That reconciliation happened in your head. Now people ask a model and paste what it gives them, and the model isn’t doing that reconciliation for you.

The training data skews old. The public writing about any library that’s been around a while is weighted toward its early, most-discussed years - every retired function, every old argument, every outdated convention, still indexed, still upvoted, still sitting in a tutorial nobody updated. More documentation doesn’t fix an unstable API. It just preserves every past state of it as competing advice with no dates attached.

And confidence doesn’t track how current something is. A stale forum post at least has a timestamp. A model’s answer doesn’t. It’ll hand you a deprecated idiom as fluently as a current one, and you won’t know anything’s wrong until it breaks - or, if the behavior just quietly changed instead of breaking outright, maybe you never find out at all.

Put together: the older and more heavily documented a moving API is, the more likely a model is to hand a newcomer something subtly wrong. Which runs against the usual instinct that more documentation is always better.

A few rules I try to follow

None of this is new advice. What’s changed is the cost of ignoring it.

Keep the surface small and orthogonal - a handful of verbs that compose (fit, evaluate, tune, cv, compare, interpret, estimate) beats fifty specialized functions. Fewer entry points means fewer things to explain, fewer ways to get it wrong, and less that can drift out from under you. It also means one person can actually hold the whole thing in their head, which is the only way it stays coherent for years.

Own your contracts. Decide for yourself what a fitted object contains, what predict returns, how a factor outcome gets handled, what a probability column looks like. Hand those decisions off to a stack of upstream packages and every design change they make becomes a change in your behavior that you didn’t choose and can’t veto.

Prefer shallow dependencies on stable parts. Calling ranger, glmnet, xgboost, and base stats directly means each dependency is small, single-purpose, and hasn’t moved much in a decade. Building on a large coordinated ecosystem instead means you’ve signed up to track that ecosystem’s lifecycle forever. Both are dependencies - only one of them is an ongoing maintenance bill.

Treat deprecation as a failure, not a feature. A formal deprecation process is honest and a long warning window is considerate, but a deprecation is still an admission that the interface was wrong and that everyone downstream now has work to do. For infrastructure code, the target rate of breaking changes should be close to zero. Depend on something that deprecates on a regular cadence and you’ve inherited that cadence.

Write documentation that ages well - examples that exercise the stable core rather than showing off a new argument, dates on your posts, and if you have to show an old way next to a new way, label them so there’s no ambiguity. Assume a model is going to ingest the page with zero context and reproduce whatever’s on it.

And where you can, make formulas and plain data frames the interface. They’re about the most stable contract R has, and an API built on them inherits that permanence instead of the churn of a bespoke specification object.

A pattern I distrust

The thing I’m most wary of is the wrapper built on a wrapper - a convenience package whose whole pitch is a simple front door, sitting on top of a deep, still-evolving stack of other packages.

I get the appeal. You get breadth fast, you inherit a big community’s testing, and the individual pieces are often genuinely good. But the reliability of the result isn’t something the author of the top layer actually controls - it’s the union of every layer’s failure modes underneath it. A change in low-level evaluation semantics, a reordering of columns somewhere in preprocessing, a shifted default three packages down, and any of it can surface as a wrong answer at the top, with the top-level author only able to wait it out or work around it.

You can see this in the release history of packages like this: lots of releases in a short window, most of them not adding anything new but fixing core behavior that turned out to be wrong, or patching around something upstream that broke. That’s not a package hardening on solid ground. That’s a package discovering its ground keeps moving - which is the opposite of what a long release history is usually taken to mean.

Automated ML wrappers get hit hardest here, since their entire job is to hide the stack. When the hidden stack shifts, users have no way to even notice, and the model they ask for help has been trained on years of the stack’s older shapes. The abstraction meant to protect the newcomer ends up being exactly what stops them from seeing what went wrong.

None of this is against large ecosystems in general - they’re fine for prototyping, fine for learning. It’s against building one underneath a package that’s marketed on durability and then acting surprised when the durability doesn’t show up.

Why I actually care about this

There’s a question underneath all of this about what we think a library even is.

One way to see it: a library is alive. It grows, sheds old parts, follows whatever the current best thinking is, and users are expected to move along with it. Deprecation is just healthy metabolism, and the old code sitting out there in the world is dead weight - not really the project’s problem if a model keeps resurrecting it.

The other way: a published interface is closer to a promise. Once people build on it, its shape stops being entirely yours to change. Base R takes this line - lm, predict, model.matrix have meant the same thing for decades, and that’s exactly what let a whole scientific literature of reproducible code pile up on top of them. The value isn’t in any one function being elegant. It’s that code written against them in 2005 still runs and still means the same thing today.

I hold the second view, and honestly the arrival of models that write code for people has only made me more sure of it. We’re now writing interfaces that will be consumed at scale by something with no sense of time, and everything we ship becomes training data eventually. A stable, modest, self-owned API is a small courtesy to a future where most of the code calling your library was written by something that learned it from your past, not your present.

Wanting to build something that lasts, with few parts and visible joints, has always been partly an aesthetic preference. Now it also happens to be the practical one. If the main reader of your code is going to be a model averaging over your history, the best thing you can do is not give it much history to average over.


Back to top