Chapter 06 of 12
Backward Compatible API Changes
Additive evolution, the tolerant reader pattern, expand and contract, and how to prove an API stayed compatible instead of assuming it did.
Additive evolution is the version you never have to cut. It sounds like a constraint and it works more like a design discipline, because most contract changes have a compatible form if you look for one before you reach for a version number.
The safe set
Five changes are compatible under every reasonable client, and between them they cover a surprising amount of ordinary product work.
- Add an optional request parameter, with a documented default
- Add a field to a response
- Add a value to an enum a consumer sends back to you, once you have said new values may appear
- Relax a required field to optional
- Add a whole new endpoint
The pattern is that obligations may only shrink and payloads may only grow, which is the asymmetry from what counts as a breaking change. Every safe change fits it, and every unsafe change violates it.
Tolerant readers
Additive evolution only works if clients tolerate growth, and clients only tolerate growth if somebody told them to.
A tolerant reader takes the fields it needs and ignores everything else. It does not validate against a closed schema, it does not fail on an unfamiliar enum value, and it does not depend on the order of members in an object. Write that expectation into your documentation, because a consumer who chose strict validation without knowing your evolution policy made a reasonable decision with bad information.
Two smaller rules make a real difference in practice. Say that unknown fields must be ignored, and say that new enum values may appear with a documented fallback for unrecognised ones. Both cost you a sentence each and remove entire categories of future breakage.
Expand and contract
The pattern behind almost every avoided version is the same three-step move.
Expand. Add the new field, parameter or endpoint next to the old one, and populate both of them so that callers on either side receive correct data.
Migrate. Mark the old one deprecated in the specification and in the documentation, tell consumers what to use instead, and watch usage fall.
Contract. Remove the old one when nobody calls it any more. That removal is a breaking change on paper and nobody notices it, because the affected set is empty.
The step teams skip is the middle one, and skipping it is what turns expand and contract into permanent duplication. A deprecation without a date is a field you will still be populating in four years, which is covered in deprecation and sunset.
Renaming without breaking
Renaming is the most common request and the least necessary breaking change.
Serve both names for a period. On responses, populate the old field and the new one with the same value, so a consumer can move whenever it suits them. On requests, accept either and prefer the new one when both arrive, and document that behaviour so nobody discovers it by experiment. Then deprecate the old name with a date, and remove it when the traffic is gone.
The same applies to endpoints. A new path serving the new shape, the old path still answering, and a Deprecation header on the old one is the entire migration, spread over as long as your consumers need.
Compatibility is a claim until you check it
Everything above describes intent, and what actually ships is a different question. A serializer that starts omitting nulls, a library upgrade that changes date formatting and a field that quietly becomes required in the database all break consumers without anybody deciding to.
That is why additive evolution needs verification and not just discipline. Two checks cover it, and they answer different questions.
| Check | Question it answers |
|---|---|
| Contract diff between two versions | Did we intend a breaking change |
| Contract validation against live responses | Did the running service produce one anyway |
The first runs before merge and needs only the two documents. The second needs a real environment and real responses, because a document cannot tell you what a service does. Contract testing covers the mechanics of the second one in detail.
When additive stops being honest
There is a point where compatibility becomes a costume. An endpoint with three parallel field names, two of them deprecated and one of them authoritative under conditions nobody remembers, is technically unbroken and practically unusable.
The signal is when the answer to a consumer's question needs a paragraph. At that point the compatible path has become more expensive than the version would have been, and cutting the version is the kinder decision.
In Routebase
Contract tests validate live responses against the published contract, so an additive claim gets checked instead of trusted. A field that changed type, a required property that went missing, or a shape nobody documented fails the run even when the status code is still 200.

After the release the same validation continues as a monitor against a deployed environment, and the difference is reported at field level with the request and response that produced it. Where an endpoint was deliberately deprecated instead of removed, the deprecation travels with the specification, into the exported OpenAPI and onto the portal page a consumer reads. The Contract Drift guide covers the monitoring side, and the Deprecation guide covers the middle step of expand and contract.
Frequently asked questions
What is a backward compatible API change?
A change is backward compatible when every caller that worked before the change still works after it, with no edits on their side. In practice that means growing the payload rather than reshaping it, adding optional inputs rather than required ones, and never removing or retyping anything a consumer already reads. The list of safe changes is short and it covers most of the work an API does over its life.
What is a tolerant reader?
A tolerant reader is a client that takes only the fields it needs and ignores everything else, including fields it has never seen. It does not validate responses against a closed schema, it does not fail on an unrecognised enum value, and it does not depend on the order of object members. Tolerant readers are what makes additive evolution possible, which is why the expectation belongs in your documentation.
What is the expand and contract pattern?
Expand and contract turns one breaking change into two compatible ones separated by time. First you add the new field or endpoint alongside the old one and populate both. Then you mark the old one deprecated and watch its usage fall, and you remove it once nobody calls it. The cost is a period of duplication, and the benefit is that no consumer is broken at any point.
How do you prove an API change was backward compatible?
Compare the two contract documents with a fixed rule set to catch structural breakage, then validate real responses from the running service against the published contract. The first check tells you what you intended, and the second tells you what the service actually does. Without the second one, additive evolution is a claim nobody has verified.
Last reviewed by The Routebase Team.