Back to Writing
Growth status: Evergreen EvergreenUpdated: Apr 19, 20264 min read

The API is a Promise: Designing for Systems You No Longer Control

Over time, you learn that clean code is an internal concern, but stable interfaces are external. Users do not care how your system is structured. They care that it behaves the same way today as it did yesterday.

I remember the exact moment I realized I had lost control of my own code. It wasn’t a bug or a crash. It was a mobile developer in another time zone who had built an entire feature on top of a temporary field in a JSON response.

I wanted to rename that field to something more sensible. It would have taken seconds. But the moment that response left my server and reached a device I didn’t own, it stopped being my code. It became a promise. And in software, breaking a promise is far more expensive than refactoring a function.

The Illusion of Agility

In the early stages of a system, speed feels like the only thing that matters. You move quickly, reshape your database without hesitation, and adjust responses as if nothing depends on them. This works because nothing really does. The frontend and backend move together, often in the same repository, deployed at the same time. You can afford to be clever.

That illusion fades as soon as your system escapes your control.

A mobile app might sit on a user’s phone for months without an update. A partner might integrate your API into a dashboard you never see. At that point, your backend is no longer just code you can change freely. It becomes infrastructure that other people rely on.

This is where many experienced engineers still make a critical mistake. They treat APIs like private functions. A private function can change whenever you want. An API cannot. Its shape is no longer an implementation detail. It is a contract.

Design for the No

Good API design is not about flexibility. It is about constraint. Every piece of flexibility you expose becomes something you are responsible for supporting indefinitely.

A common example is the temptation to include a generic metadata or settings field that accepts arbitrary JSON:

type UserProfile struct {
    ID       string                 `json:"id"`
    Settings map[string]interface{} `json:"settings"`
}

This looks harmless, even convenient. But the moment it is released, someone will store complex, deeply nested data inside it. At that point, you have lost control. If you later need to restructure or optimize how that data is stored, you cannot do it cleanly. You are forced to carry that decision forward, parsing and maintaining a structure you never truly defined.

A more constrained design feels slower, but it preserves your ability to evolve:

type UserProfile struct {
    ID                string `json:"id"`
    IsNotificationsOn bool   `json:"is_notifications_on"`
    ThemePreference   string `json:"theme_preference"`
}

Here, the boundaries are explicit. You decide what is supported, and you retain the freedom to change how it is implemented internally. Constraint is not a limitation. It is what protects your system from accidental commitments.

Versioning is a Strategy

Versioning is often treated as a technical detail, something you solve with a path or a header. In reality, it is a long term commitment.

Every version you release becomes something you may need to support far longer than expected. As long as even one client depends on it, it cannot simply disappear. That means every additional version adds weight to your system and slows down your ability to move.

The alternative is not to avoid change, but to approach it differently.

Instead of replacing fields, add new ones. Instead of silently altering behavior, make transitions visible. Instead of exposing everything, return only what is necessary. These decisions reduce the need for hard version boundaries and allow your API to evolve without breaking existing clients.

Deprecation should also be gradual and observable. Logs, warnings, and clear communication give consumers time to adapt. Turning things off without a transition is rarely a technical failure. It is a failure of design.

The Reality of Ownership

Over time, you learn that clean code is an internal concern, but stable interfaces are external. Users do not care how your system is structured. They care that it behaves the same way today as it did yesterday.

Designing an API means accepting that you no longer control how it is used. Someone will depend on it in ways you did not expect, on systems you cannot see, and they may not update their code for a long time.

If your design can survive that reality, then you are not just writing code. You are building something that can endure. You are creating systems that keep their promises.

Share this writing