Reference Guide · APIs

API Design for Long-Lived Systems

An API is the longest-lived thing a system exposes to the outside world — a promise to other people's code that you cannot unilaterally take back. How to design interfaces you can keep evolving for years without breaking that promise. A decision document for CTOs, lead developers and software architects.

What is this? · Reference Guide

A solid guide to an engineering question — with trade-offs, costs and the case in which we decide differently. Not an opinion piece, but a reference text. Go to overview

Author
Batunet Engineering
Reading time
17 min
Level
In depth
Status
Approved
Last reviewed
21 July 2026
Updated
21 July 2026
On this page

The code behind an API can be restructured, replaced or thrown away at any time — it belongs to you. The API itself only half belongs to you. The other half belongs to every external system that relies on its shape, and you cannot touch that half without reaching into someone else's production. That is exactly what makes API design one of the least forgiving decisions in software engineering: almost everything on the inside is reversible; the shape visible from the outside hardly is.

This document looks at interface design from precisely that angle — not how to build an API, but how to design it so you can keep evolving it for a decade without breaking its consumers. It is deliberately neutral with respect to frameworks, protocols and vendors: whether the interface is built in a REST style, as RPC or as a query-based protocol does not change the principles. The concrete mechanics of versioning have their own, deeper article; this one is about the design decisions that come before.

1. APIs are contracts

An API's contract is everything a caller can rely on: the structure of the response, the meaning of the fields, the semantics of errors, the behavior in edge cases. Much of this is written down nowhere in the documentation — it lives in the code of consumers who observed a particular response shape and built on it. The real contract is therefore larger than the promised one: it covers everything observable, not just what is documented.

documented observed — the real contract field order, edge cases, timing, error shape visible consumers rely on it anyway

Diagram: What you guarantee is the tip. What consumers rely on reaches far below it — every observable property becomes part of the contract for someone.

This leads to the basic stance of the entire document: changing an API does not mean changing your own code; it means reaching into the systems of people you will never meet. Every design decision should therefore be tested against what it will demand of callers over the years — not what it saves the provider today.

There is a sober rule of thumb for this: once an interface has enough consumers, every observable property becomes a dependency for someone — regardless of whether it was ever promised. If you don't want to guarantee something, don't make it observable in the first place. Every detail that leaks out — an internal field, an incidental sort order, an error message exposing technical internals — is a silent part of the contract that you can later only withdraw by breaking it. Keeping the contract surface lean is therefore not austerity but precaution: the less is visible, the more remains changeable.

Trade-off. Taking the contract seriously means exposing less than would be technically possible — the price is less up-front convenience for the provider.

Cost. The discipline of treating every response as a commitment demands care in design and restraint in revealing internal details — effort that only pays off over the lifetime.

When we decide differently. For a purely internal interface whose two sides are rolled out together and in sync by the same team, tight contractual binding is overkill; there you can change things more freely, because there is no external code to surprise.

2. Stability over convenience

The most common design mistake stems from an understandable motive: shaping the API in whatever way is easiest for the provider at the moment. Mirroring internal data structures directly to the outside, tacking on every new capability as yet another field, aligning behavior with the current implementation. Each of these conveniences is cheap today and becomes a shackle tomorrow, because it becomes part of the contract the moment a consumer observes it.

The guiding principle is therefore: an API is designed for the consumer, not for the producer. It should express the domain, not the internal implementation — because the implementation will change, while the domain is more stable. An interface that lets internal details shine through couples external systems to decisions you actually wanted to remain free to change. You buy stability on the outside with the willingness to translate on the inside.

Trade-off. A deliberately designed contract surface, decoupled from the internals, costs a translation layer — the gain is the freedom to change the internals without touching the contract.

Cost. This translation is ongoing work: every internal change has to be checked for whether and how it surfaces at the boundary.

When we decide differently. For a prototype or a short-lived interface, mirroring internal structures directly is the faster and correct choice; decoupling only pays off once the API lives long and carries external consumers.

3. Designing for change

A long-lived API will certainly change — you just don't know how. The only reliable way to handle that uncertainty is to design the interface so that it tolerates change instead of forbidding it. Concretely, that means choosing shapes that can grow additively without breaking what already exists.

In practice, this means making responses and inputs extensible. Treating enumerations so that an unknown value does not crash a client. Designing objects so that a new, optional field can be added later without shifting the meaning of the existing ones. Not guaranteeing more than necessary — every property locked in beyond what is needed is one you can no longer change later. The second half is the tolerant reader on both sides: strict in what you guarantee, generous in what you accept. A client that ignores unknown fields allows the server to grow; a server that does not immediately reject unknown input lets clients build ahead.

Designing for change also covers how operations are cut. An interface that clings closely to the internal data shape forces consumers to assemble many small calls into one business action — and thereby ties them to today's structure. Operations that represent a business intent rather than a table survive internal restructuring better, because the internals may change as long as the intent stays the same. Cutting along the domain is therefore not a matter of style but a decision about lifespan: what expresses intent endures; what mirrors structure ages along with it.

Trade-off. Designing for change means locking in less and leaving more cases open — the price is an interface that initially seems less strict but is more elastic.

Cost. Tolerant readers and extensible shapes require more care in validation and testing, because you also have to deliberately define the behavior for the unknown.

When we decide differently. Where maximum strictness is a business requirement — for instance in heavily regulated interfaces that must reject every deviation — you deliberately choose the narrow, less elastic shape and accept that changes become more expensive.

4. Backward compatibility

For an interface with external consumers, backward compatibility is not a courtesy but the default. The decisive line does not run between small and large changes, but between those an existing caller notices and those it does not.

ChangeBreaking?Why
New optional field in the responsenoold clients ignore it
New endpoint / new operationnonobody calls it yet
New permitted value in an extensible enumerationno**only if clients tolerate the unknown
Removing or renaming a fieldyesexisting expectation violated
Changing a field's type or formatyesconsumers' parsers break
Making an optional field requiredyespreviously valid calls become invalid
Tightening validation, lowering limitsyespreviously accepted calls fail
Shifting the meaning of an error codeyesconsumers react incorrectly

The uncomfortable part: some consumers rely on behavior that was never promised — on the order of fields, on incidental timing, on an unspecified limit. That is why the safest assumption is that every observable change is breaking for someone until proven otherwise. Compatibility is thus less a rule than an attitude of caution.

Trade-off. Backward compatibility as the default buys reliability at the cost of legacy baggage: you carry along outdated fields and shapes you would choose differently today.

Cost. The compatible solution is rarely the most elegant one; you accumulate optional fields and live with names you regret — the price of breaking no one.

When we decide differently. When the model has become fundamentally wrong and every compatible contortion only obscures the contract further, a deliberate, cleanly versioned incompatible change is more honest than a chain of compromises.

5. Versioning strategy

A new version is expensive — for the provider, who runs two contracts, and for the consumers, who have to migrate. That is why it is the last resort, not the first reflex: most things can be solved through compatible evolution if the API was designed for change from the start. A version is an admission that you can no longer move forward compatibly — you should choose it rarely and deliberately. The mechanics behind it — parallel versions, deprecation, a clean exit — are covered in detail in API versioning without breaking customers; here, the only concern is the design decision of where the version is carried in the first place.

Location of the versionAdvantagePrice
In the path (visible in the address)easy to see, easy to route, cache-friendlyencourages whole new versions instead of fine-grained evolution
In the header / metadataseparates the version from the resource, fine-grainedless visible, easier to overlook, harder to cache
In the media type (negotiation)close to the protocol, expressivemore complex for consumers, higher barrier to entry

None of these options is superior; each trades visibility against granularity differently. The choice follows from who the consumers are and how they build — not from a general ranking.

Trade-off. Every placement of the version buys one property — visibility, granularity, closeness to the protocol — at the cost of another.

Cost. Regardless of location, every additional version you operate doubles the maintenance: two contracts, two test chains, two migration paths.

When we decide differently. For a small, controlled consumer base you can support directly, a visible version in the path is often enough; for a large, heterogeneous consumer base with fine-grained evolution steps, carrying the version in the metadata can be the gentler choice.

6. Error handling

Errors are part of the contract, not an edge case beside it. A caller builds not only on the success response but just as much on what comes back when something fails — and on how it is supposed to react. An API whose errors are unclear, inconsistent or erratic is hard to use even when the success path is flawless.

A practical litmus test: a client should be able to react to an error without reading its text. The text is for humans and may change — into another language, for example; the code is for machines and must remain stable. Anyone who mixes the two and forces consumers to check for wording turns the message itself into the contract and loses the freedom to ever rephrase it.

Three principles carry the design. First: errors must be distinguishable and machine-readable — a stable, classified code a client can react to reliably, not shifting free text. Second: the separation between a caller error and a provider error must be clear, because it determines whether the caller may retry or must correct its request. Third: an error must not reveal internal details — no internals, no technical traces that would become part of the contract and that you could never withdraw later. Error semantics must remain as stable over time as success semantics; reinterpreting an error code is a break like any other.

Trade-off. A cleanly designed, stable error model costs design work up front and the discipline not to change it casually — the gain is that consumers can react to errors reliably.

Cost. Stable, classified errors mean maintaining another contract surface and exercising the same caution when changing it as with success responses.

When we decide differently. For an internal interface with a single consumer that grows alongside it, the error model may remain leaner and more informal; full rigor only pays off with external consumers you cannot coordinate.

7. Idempotency

As soon as an API is reachable over a network, it will at some point receive the same request twice — through a retry, an ambiguous timeout, a redelivery. That is not a bug but a property of distributed communication. A long-lived API design embraces this instead of ignoring it.

The design answer is idempotency: shaping operations so that executing them multiple times has the same effect as executing them once. Reads are idempotent by nature; so are setting a value or deleting by identifier. Operations that change state or move money are not idempotent on their own — they need a mechanism, such as an idempotency key supplied by the caller, by which the provider recognizes a retry and does not act again. If the contract does not provide for this property, every consumer is forced to work around its absence later. The depth of this topic — keys, concurrency, delivery — is covered in Idempotency in distributed systems; for design, what matters is planning for it from the start.

Trade-off. Building idempotency into the design costs additional mechanics — keys, checks, storage — but buys the ability to retry safely without causing damage.

Cost. Detecting retries requires state and care around concurrency; that is real effort, incurred for every write operation.

When we decide differently. For pure reads and naturally idempotent operations, you don't build a key mechanism; the effort applies only to operations that are not inherently repeatable.

8. Evolution without breaking customers

The goal is an API that grows for years without ever surprising a consumer. That works when changes are predominantly additive and the rare incompatible steps unfold as an orderly process rather than an event. An interface should feel like a place where new things arrive without old things disappearing.

API contract Consumer Consumer Consumer external system external system external system

Diagram: One contract, many consumers you don't control. A breaking change hits all of them at once — which is why an orderly exit is not optional but mandatory.

When an incompatible step becomes unavoidable, deprecation is the way: mark the old shape as deprecated, offer the new one in parallel, name a clear shutdown date and communicate it before it arrives. The exit must not surprise anyone. Running two versions in parallel for a while is the price of letting consumers migrate at their own pace rather than under duress.

Trade-off. Orderly evolution buys reliability for consumers at the cost of double operation and long patience before switching off the old.

Cost. Parallel versions and a clean deprecation process tie up capacity permanently — for communication, for parallel operation, for supporting the migration.

When we decide differently. With a small, known consumer base you can reach directly, a short, closely supported switchover can be cheaper than long parallel operation; the larger and more anonymous the consumer base, the longer and more formal the process has to be.

9. Common mistakes

The recurring patterns on which long-lived API design fails — almost all of them are variants of the same mistake, designing the API for the provider instead of the consumer:

  • Mirroring internal data structures directly to the outside, thereby coupling external systems to your own internals.
  • Guaranteeing more than necessary — field order, timing, unspecified limits — which later becomes a contract you cannot terminate.
  • Designing enumerations and objects rigidly, so that no new value and no new field can be added additively.
  • Shaping errors as free-form text instead of stable, classified codes that consumers could reliably react to.
  • Letting internal details leak into error messages and thereby involuntarily making them part of the contract.
  • Retrofitting idempotency only after the first duplicate bookings have occurred, instead of providing for it in the contract.
  • Versioning too early and too generously, replacing fine-grained evolution with expensive full versions.
  • Switching off an old version without an announced date and breaking consumers without warning.
  • Changing observable behavior on the assumption that it is not part of the contract because it was never documented.

10. Decision checklist

Before and during the design of a long-lived API, clarify in order:

  • Designed for whom? Does the interface express the domain for the consumer — or does it mirror the producer's internal implementation?
  • Deliberate contract surface? Is it clear what is guaranteed, and is as little as possible beyond that observably locked in?
  • Can it grow additively? Can new fields, values and operations be added without breaking what exists?
  • Tolerant reader? Do both sides ignore what they don't know instead of breaking on the unknown?
  • Errors as contract? Are errors stable, classified, machine-readable and free of internal details?
  • Client and server errors separated? Can a caller tell whether it may retry or must correct?
  • Idempotency provided for? Are operations that change state or move money designed so that a retry does no harm?
  • Versioning as a last resort? Has it been decided to solve as much as possible compatibly — and where the version will be carried if it becomes necessary?
  • Orderly exit? Is there a deprecation process with parallel operation and an announced date before anything is ever switched off?

If you cannot answer these questions, you are not designing a long-lived interface but a promise you will break later.

FAQ

What is the most common mistake in API design? Designing the API for the provider instead of the consumer — usually by mirroring internal data structures directly to the outside. That is the most convenient option in the moment and couples external systems to your own internals, which you actually wanted to remain free to change. Almost all other mistakes are variants of this one.

Doesn't backward compatibility eventually become a millstone? It has a cost, yes — you carry outdated shapes along. But the alternative, breaking consumers, costs more: lost trust and silent failures in external systems. The compatible solution is rarely the most elegant, but almost always the cheaper one. If the model becomes fundamentally wrong, a cleanly versioned incompatible change is the honest way out.

Which versioning is right — path, header or media type? None is superior. The path is visible and easy to route but tempts you into whole new versions; header and media type are more fine-grained but less visible and more complex. The choice follows from who the consumers are and how they build — not from a ranking.

Why are errors part of the contract? Because consumers have to react to them. A client branches on the error — retry or correct. If errors are unclear or erratic, the API is hard to use even when the success path is flawless. That is why error codes must be as stable as success responses, and reinterpreting them is a break like any other.

Does every API really need idempotency? No. Pure reads and naturally idempotent operations already have it built in. The effort applies to operations that change state or move money and are not inherently repeatable — but there it pays off, because sooner or later the network will force retries.

Should we design the API first or the implementation? The contract first. If you build the implementation first and derive the API from it, you almost inevitably mirror internals to the outside. A contract designed as domain, independent of the implementation, lasts longer — because the implementation may change without touching the contract.

Further reading

It is grounded in the Batunet Engineering Method: design the contract before the implementation, build for change, surprise no one.


The code behind an API is yours — you may change it at any time. The API itself you have lent out. Good design is the art of evolving it without ever reclaiming what others rely on.

Referenced entities

A concrete project in this field?

Reference Guides show how we think. For your system, talk to our management — technical, no sales pitch.