An API can gain useful capabilities without requiring every consumer to update at the same time. Doing that reliably takes more than retaining old field names. Existing requests must keep their meaning, old readers must understand the results they receive, and new workflows must not silently invalidate old assumptions.
Backward-compatible design preserves those supported interactions while creating room for new ones.
This chapter develops practical design techniques through a fictional order API: stable defaults, explicit opt-ins, controlled response growth, and updates that preserve data an older client does not understand.
Start by identifying the promises that the interface already makes. For an order API, these might include the meaning of fulfillment states, monetary units, required response fields, access rules, and whether an acknowledged operation has completed.
Keep those promises in the API layer rather than letting database structure determine the response. A contract adapter is code that translates between the service's internal model and a consumer-facing representation. It gives internal changes a place to stop before they reach callers.
Suppose the service replaces one delivery table with several internal records. The adapter can still produce the original delivery summary if the new records contain enough information to calculate it accurately.
The diagram shows this separation:
Both interfaces use the same underlying business state. The adapter preserves meaning; it does not maintain a second, potentially contradictory copy of the order.
This approach has a limit. If the old contract promises one tracking number for the package containing every item, no translation can truthfully satisfy that promise when the order has two packages. An adapter can derive a valid summary, but it cannot manufacture compatibility when the original model no longer represents the business state.
An optional input is useful only when its omission has defined behavior. For an existing operation, choose an omission behavior that preserves the supported interaction.
Assume order creation currently selects standard delivery. A new deliverySpeed input can let consumers explicitly request express delivery. These JSON examples are request-body excerpts; other order inputs are unchanged.
An existing client continues to send:
A client requesting the new capability sends:
For this fictional API, define the new input precisely:
The service must evaluate eligibility, permissions, and any required price acceptance before creating the order. Assume the existing checkout process already requires acceptance of the applicable total; express delivery must participate in that process rather than introducing an undisclosed charge.
The branching rule is small but important:
Old callers never select the new branch. Avoid changing omission to mean “choose the fastest available delivery,” because that would change existing requests as warehouse capabilities change.
Keep creation defaults separate from update semantics. On a partial update, omission of deliverySpeed should preserve the stored selection under the update contract, not reset it to the creation default. Otherwise an older client editing a delivery note could accidentally change an express order to standard delivery.
Add information in a way supported readers can accommodate. For a JSON API, the contract can explicitly allow new response properties and require consumers to ignore properties they do not recognize. Establish that rule before relying on it, and make supported SDKs behave accordingly.
This does not permit changes to existing field meanings, types, or presence guarantees. Nor does ignoring unknown properties mean accepting malformed known fields. A reader should still reject an invalid monetary amount or avoid acting on an unusable identifier.
For a capability that requires additional computation or permissions, explicit selection can keep the ordinary response predictable. Suppose an order lookup adds include=deliveryEstimate. This parameter is a convention of the example API, not a standard HTTP parameter.
An authorized client requests https://api.store.example/orders/ord_4821?include=deliveryEstimate over HTTPS. The token is a placeholder:
The body is the single line in the example without a trailing newline. Assume this endpoint's established representation contains id and fulfillmentStatus. The estimate is a planning window that uses calendar dates at the delivery destination, not a guaranteed arrival deadline.
Define edge cases as part of the feature. In this design, an authorized lookup without include returns the established representation and does not compute an estimate. With the include selected, a temporarily unavailable estimate returns {"status":"unavailable"} inside deliveryEstimate; the original order fields remain present. An unrecognized include value produces a validation error. An authenticated caller who may read the order but lacks the additional estimate permission receives a forbidden response when explicitly requesting the estimate.
These are deliberate choices. The new permission must not become a prerequisite for the ordinary lookup, and estimation failure must not introduce a new dependency into requests that omit the include.
If exposing estimates needs a substantially different access model or workload, a separate operation may be clearer. Choose based on the consumer task and isolation needed, rather than adding an include option for every conceivable detail.
Compatibility depends on both sides following clear extension rules. A useful rule for this API is that response objects may gain properties, while requests may contain only documented writable properties.
That asymmetry is intentional. Ignoring an unfamiliar response property can let an older reader continue using known data. Silently ignoring a misspelled request field such as deliverySpeeed can make a caller believe the API selected express delivery when it did not. Validate new commands precisely while preserving previously accepted inputs.
Apply the rule to SDKs as well as handwritten clients. A library that rejects all extra response properties does not implement an open-object contract. Updating the documentation alone cannot fix older copies of that library; the service must account for the supported installed clients before sending additions to them.
Unknown properties and unknown values need separate policies. An unfamiliar detail property may be irrelevant to an old client. An unfamiliar value in a field controlling fulfillment may determine whether the client can safely proceed.
For a newly designed extensible status field, specify what an older consumer should do when it encounters a new value. A display-only consumer might show “Status unavailable” while retaining the raw value for diagnostics. An automation that releases goods should stop that action until it recognizes a state authorizing release.
Do not map unknown values to a familiar successful state just to avoid a parser error. That trades a visible incompatibility for an incorrect business action. If an existing field promised a closed set of values, introducing a fallback requirement now does not make old consumers support it retroactively.
Keep identifiers opaque and define their permitted size rather than encouraging consumers to infer database structure from them. Keep monetary units explicit and preserve them. Establish collection limits and continuation behavior before consumers depend on unbounded results.
These decisions create predictable room for change, but only within the documented bounds. An opaque identifier can still exceed a consumer's supported storage length, and a paginated response can still break callers if its continuation rules change.
Reading an added field safely is only half the problem. An older client may read a resource, modify a known property, and write it back without the new property.
Suppose the service adds a writable deliveryInstructions field. A newer client saves “Use the side entrance.” An older client then changes customerReference using a model that has no place to store delivery instructions. If the update replaces all writable state and treats omitted instructions as empty, the old client erases the newer data.
For interfaces that use partial updates, apply changes only to fields the caller explicitly selects. A field mask is an explicit list of fields an update intends to modify. Alternatively, a documented partial-update format can determine the changed fields from the submitted document. Either approach needs defined omission and clearing behavior.
For example, an update selecting only customerReference should leave deliveryInstructions and deliverySpeed untouched. Explicitly clearing instructions should require the operation's documented clearing value or action. The provider must also check whether the caller may write each selected field; being readable does not make a property writable.
This protection addresses fields the old client never knew existed. It does not solve two clients concurrently editing the same selected field; that still requires the API's concurrency controls.
Do not silently reinterpret an established full-replacement operation as a partial update. Some consumers may depend on its replacement behavior. Introduce a suitable update capability deliberately, and evaluate how existing writers interact with newly stored state. There is no universal rule that the server should always preserve omitted data or always clear it; the operation's existing contract determines the starting point.
An explicit opt-in isolates the initiating request, but it does not automatically isolate everything that request creates. Several clients with different release schedules often read the same orders.
The following diagram shows how new behavior can reach an old consumer indirectly:
The warehouse integration and notification worker did not opt in, yet they encounter the resulting order. Before enabling express delivery, establish whether they can still perform their supported tasks accurately.
Express delivery may be compatible if the existing warehouse interface already conveys the applicable dispatch deadline and needs no new instruction. It may be incompatible if workers must choose a new carrier service but old clients have no way to represent that selection. Preserving a response shape while withholding essential fulfillment instructions would not solve the problem.
Trace the capability through subsequent reads, updates, events, and background processing. Include older writers that might reset new state and older event handlers that might misinterpret it.
If a new workflow conflicts with old client assumptions, you may temporarily limit it to an environment where all affected clients support it, but only if you can enforce that restriction. A user interface toggle alone is insufficient when shared orders remain visible to other integrations. If there is no truthful representation or enforceable boundary, acknowledge that the change requires consumer coordination rather than claiming backward compatibility.
Capture preserved behavior in checks that use the existing interface. Running only updated clients against the updated service can hide a dependency on new fields or new defaults.
For the order examples, the important evidence includes:
Use old client artifacts where practical, not regenerated clients that already understand the addition. Inspect business outcomes and side effects alongside request success and response parsing. Keep schema checks for structural changes, but supplement them with assertions about defaults, access, and updates.
The checks should express stable promises rather than freeze incidental details. If the contract allows explanatory error text to change, verify the documented machine-readable code and recovery behavior. If response properties may expand, avoid comparing raw JSON strings in a way that forbids those additions.
Preserving every historical quirk indefinitely can make an API expensive to operate. Keep compatibility behavior explicit and limited to real commitments, and record known accidental dependencies separately. If preserving a contract requires misleading data, unsafe access, or behavior the service can no longer support, adding an optional field cannot solve the problem.
Backward-compatible design gives old requests stable behavior and new capabilities explicit rules. Keep the public contract separate from internal storage. Preserve what happens when callers omit fields, define how clients handle new fields or values, and prevent older updates from erasing fields they do not understand.
Check the full path of new state through readers, writers, and event consumers. Verify compatibility with existing clients and meaningful behavior checks, and recognize when the old contract can no longer describe the new workflow truthfully.