Tax / Fiscal · Evolution

API Evolution Strategies

API Evolution Strategies is the work that makes the systems talk. The API is the contract between the producer and the consumer; the contract is what determines whether the.

John Kihiu12 min read

An API is a promise to every client that already integrated against it. The hard part of API evolution isn't designing the next version — it's changing behaviour without breaking the consumers you can't see and can't force to upgrade on your schedule. Every strategy below is really just a different answer to the same question: how do you change your mind in public without breaking someone else's production system.

Additive changes first

The cheapest form of evolution is the change that doesn't require a version bump at all. Adding a new optional field to a response, adding a new endpoint, adding a new enum value a client doesn't recognise yet — these are backward compatible by construction, provided your clients follow Postel's law and ignore fields they don't understand. Most breaking changes in practice aren't forced by necessity; they're the result of not designing the schema to tolerate addition in the first place. A JSON schema with additionalProperties: false turns every future field into a breaking change for strict clients. Default to permissive parsing and you buy yourself years of additive-only evolution before you need anything heavier.

Removing is the breaking change, not adding

Renaming a field is really two operations: add the new name, deprecate the old one, remove the old one later. Treat it that way explicitly rather than as a single atomic rename, and you get a compatibility window for free.

Versioning strategies: URI, header, and content negotiation

When a change genuinely breaks the contract, you need a versioning scheme. URI versioning (/v1/orders, /v2/orders) is the most common because it's visible, cacheable, and trivial to route at the gateway or load balancer — the cost is that it encourages whole-resource forking even when only one field changed. Header versioning (a custom API-Version header, or content negotiation via Accept: application/vnd.acme.v2+json) keeps URLs stable and lets you version resources independently, at the cost of being invisible in logs and harder to test with a browser. Neither is objectively correct; URI versioning wins when your clients are external and diverse, header versioning wins when you control the client population and want finer-grained control. Semantic versioning at the API level (not just the SDK) helps only if you're disciplined about what counts as a major bump — most teams aren't, and end up on v2, v3 forever with no clear rule for when v4 happens.

Deprecation as a process, not an announcement

A deprecation notice in the changelog is not a deprecation process. A real process has four parts: an announced sunset date, a machine-readable signal (the Deprecation and Sunset HTTP headers, RFC 8594, are the standard way to do this), usage telemetry so you know who's still calling the old endpoint, and direct outreach to the accounts still generating traffic in the final weeks. Cutting off an endpoint because "we announced it six months ago" without checking who's still on it is how you end up reverting a deprecation at 2am.

HTTP · DEPRECATION HEADERS
GET /v1/invoices/4521 HTTP/1.1
Host: api.example.com

HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: true
Sunset: Sat, 01 Nov 2026 00:00:00 GMT
Link: <https://api.example.com/v2/invoices/4521>; rel="successor-version"
Warning: 299 - "This endpoint is deprecated, migrate to /v2/invoices"

Running old and new versions in parallel

Supporting two major versions simultaneously is expensive — double the test surface, double the bug classes — but it's usually cheaper than a forced migration deadline. The pattern that scales is a thin adapter layer: the new version is the source of truth, and the old version is served by a translation layer that maps old shapes to new ones. This keeps the business logic in one place and confines the compatibility debt to a layer you can delete outright once usage hits zero. Trying to maintain two parallel implementations of the actual logic is how version skew bugs creep in, where v1 and v2 quietly disagree on rounding or timezone handling.

Contract testing against real consumers

Schema validation tells you the shape is right; it doesn't tell you a client will still parse the response correctly. Consumer-driven contract testing (Pact is the common tool here) flips the responsibility: each consumer publishes the contract it depends on, and the provider's CI runs those contracts before any deploy. This catches the case schema diffing misses — a client that breaks not because a field left the schema, but because it changed from a string to a number, or an array that used to always have at least one element now can be empty. For public APIs with unknown consumers, a request-diffing approach (replay production traffic against the new version, diff the responses) does a similar job without needing consumers to opt in.

A green schema diff is not a green light

Schema-compatible does not mean behaviour-compatible. Changing the sort order of a list, or the rounding mode on a currency field, passes every schema check and still breaks a client that depended on the old behaviour implicitly.

Picking a strategy for your API

Internal APIs with a single, known set of consumers can evolve fast and loose — coordinate the breaking change in a Slack channel and deploy both sides together. Public APIs with unknown, unbounded consumers need the full toolkit: additive-first design, explicit versioning, deprecation headers, and telemetry on who's still calling what. Most teams over-invest in versioning machinery for internal APIs and under-invest in deprecation telemetry for public ones — it's usually the wrong way round.

StrategyBest forCost
Additive-only changesMost day-to-day evolutionLow — needs disciplined schema design upfront
URI versioningExternal, diverse consumersMedium — encourages whole-resource forks
Header/content negotiationControlled client populationMedium — harder to observe and debug
Parallel version + adapter layerMandatory breaking change with slow migrationHigh — until old version usage hits zero
Consumer-driven contract testsKnown consumer set, CI-gated releasesMedium — requires consumer buy-in

Wrapping up

Evolution strategy is really a bet on how much you trust your consumers to upgrade on their own. Design for additive change by default, reserve versioning for the changes that truly can't be additive, and treat deprecation as a monitored process with a real signal, not a changelog entry. Get those three right and you rarely need anything more elaborate.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.