Prompt and when it applies
A public Orders REST API is already used by 600 third-party integrations and several mobile apps that cannot be forced to upgrade. The existing GET /v1/orders returns every order in one response. Each order contains a customer_name string, and status currently returns only pending or paid. The next release should expose customer data as a structured object, paginate the list, and introduce the refunded status.
The 600 integrations, current fields, and status values are interview assumptions. The binding constraint is that the provider cannot control when every consumer upgrades. Success covers a working v2, continued v1 behavior under the original contract, observable migration, discoverable deprecation dates, and recovery from a failed release without changing the promised meaning of a version.
This is a backend question because it tests API contracts, server-side representations, release engineering, and compatibility governance. It does not require a design for the entire order system or a database-sharding plan. Write down the existing contract before classifying each change. Merely saying “put v2 in the URL” does not prove that old clients remain safe.
{
"orders": [
{
"id": "ord_1",
"customer_name": "Ada Lovelace",
"status": "paid"
}
]
}What interviewers evaluate
The first signal is whether the candidate distinguishes three kinds of compatibility. Source compatibility asks whether old client code still compiles after regenerating or upgrading its SDK. Wire compatibility asks whether an old serializer can parse the new message. Semantic compatibility asks whether the same call still behaves as a reasonable consumer would expect. An unchanged field type does not preserve semantics by itself. Silently changing “return every order” to “return the first 100 orders” still produces valid JSON, but old clients lose data.
The second signal is contract-based judgment. A memorized universal table cannot represent every real client's parsing behavior. A new optional request field whose omission preserves the old behavior can usually remain in v1. Removing, renaming, or changing the type of a field breaks v1. An extra response field is a safe addition only if the contract permits unknown fields and the real SDKs ignore them. Adding a response enum value deserves extra scrutiny: an open-enum contract can allow expansion, while a closed enum, generated strongly typed SDK, or exhaustive switch without a default branch can fail.
The third signal is a runnable path connecting versions, implementation, and lifecycle. A strong answer preserves a v1 presentation layer, uses shared domain logic to produce separate v1 and v2 responses, runs contract-diff and old SDK tests before release, measures adoption, errors, and latency by consumer afterward, and eventually retires the old version with standardized deprecation signals, a migration guide, and explicit shutdown gates. A version identifier is a routing choice; it does not perform any of those tasks automatically.
Questions to clarify before answering
- Are consumers controllable? Three services inside one company that can coordinate an atomic rollout may use
expand–migrate–contract without a long-lived v2. Third parties and old mobile clients require a stable version boundary and a public lifecycle.
- Does the current contract require clients to ignore unknown fields and enum values? This determines whether an
added response field or enum value can stay in v1. Valid JSON alone is insufficient; inspect the OpenAPI contract, SDK types, and actual consumer behavior.
- How large is the list, and what reliability risk does it create? If returning all orders still meets the SLO,
pagination can exist only in v2. If an unbounded response already threatens availability, rate limits and an emergency communication path may be necessary, but silent truncation is still not a compatible change.
- Has the API already published a version-selection mechanism? Continue using
/v1and/v2when path versions
already exist, or keep the existing date header when versions are header-based. Changing the mechanism during the migration creates another client change.
- Can the provider identify each consumer and contact its owner? Stable application IDs, SDK versions, and owner
contacts enable precise migration tracking. Anonymous traffic requires more conservative retirement gates.
- What legal, contractual, or business support period was promised? A shutdown date comes from the published
policy, customer obligations, risk, and actual adoption. Another platform's support period is not a universal rule.
30-second response framework
“I would first freeze v1's written contract and observable behavior, then classify each proposal across source, wire, and semantic compatibility. A new optional field with unchanged default behavior may fit in v1. Replacing a string with an object, renaming a field, and changing an all-results list to pagination require v2. A new response enum value depends on the open-enum policy and old SDK behavior. I would share the Orders domain logic and keep only v1 and v2 representation adapters. Before release, I would run an OpenAPI diff, old-SDK tests, recorded-request replay, and end-to-end contract tests, then let known consumers opt into v2. I would monitor adoption and errors by consumer, publish migration, deprecation, and shutdown dates, and retire v1 only after its migration and obligation gates pass. At any failure point, I can roll back the v2 route or adapter while v1 remains unchanged.”
Step-by-step deep dive
Start by building a compatibility baseline. Preserve the current OpenAPI document, released SDKs, representative requests and responses, error codes, ordering, defaults, and list behavior. Sample undocumented but visible behavior too, because consumers may depend on field formats, null handling, ordering, or getting the full result set in one response. Record the consumer ID, version, request volume, and owner at the same time. Later, this separates “unused” from “used by someone the provider cannot identify.”
Then classify every proposed change:
| Proposed change | v1 assessment | Treatment |
|---|---|---|
| Add an optional request field whose omission preserves old behavior | Usually compatible | Add to v1 and test old requests |
| Add an optional response field | Conditionally compatible | Verify unknown-field policy and old SDKs first |
Replace customer_name with a customer object | Incompatible | Keep the string in v1; return the object in v2 |
| Remove or rename an existing field | Incompatible | Add the new name in a new version; do not remove it from v1 |
| Change an all-results list to pagination by default | Semantically incompatible | Define cursor and page semantics in v2 |
Add refunded to a response enum | Depends on the enum contract | Inspect open-enum rules, generated code, and exhaustive switches |
| Correct undocumented spelling that cannot affect reasonable dependencies | Still needs evidence | Prove it with consumer tests and traffic replay |
Pagination is the easy case to underestimate. Google's compatibility guidance calls out the risk of adding a finite default page_size to an API that previously returned every item: an old client can incorrectly assume that the first response is complete. Define items, next_page_token, ordering, and token-invalidity rules in v2. During the support period, v1 keeps its original semantics while quotas, response-size monitoring, and migration outreach control the operational risk.
Next, draw the version boundary. The prompt already uses path versioning, so add /v2/orders. Do not guess the version from User-Agent on the same path, and do not silently route v1 to a representation with new semantics. Version only the external representation. Parse each request into the same domain command, share order-query and authorization logic, then use V1OrderPresenter or V2OrderPresenter to produce the corresponding shape. Security fixes and business rules can still reach both versions without duplicating the service.
An illustrative v2 response is:
{
"orders": [
{
"id": "ord_1",
"customer": {
"display_name": "Ada Lovelace"
},
"status": "paid"
}
],
"next_page_token": "eyJvcmRlcl9pZCI6Im9yZF8xIn0"
}Use four release gates. First, compare the old and new OpenAPI definitions and reject v1 field removal, requiredness changes, type changes, and accidental new validation rules. Second, compile and run fixed contract cases with the last public v1 SDK, including unknown fields, nulls, error responses, and enums. Third, replay representative sanitized requests and compare status codes, critical fields, and ordering between the old and new v1 implementations. Fourth, let a small set of known consumers opt into v2 in a sandbox or canary environment and observe functionality, 4xx, 5xx, latency, and response size. A schema diff can find structural changes; it cannot replace semantic assertions about ordering, defaults, or page completeness.
Migration begins as opt-in. Publish v2 documentation, SDKs, a field-by-field migration table, and a sandbox that supports both versions. Show each known consumer its v1 call volume, failing endpoints, and target date. Migrate the provider's examples and official SDKs first so gaps in the guide surface early. Review adoption by consumer and inspect aggregate requests separately: a low-volume month-end reconciliation integration can matter more than many health checks. Track unique active consumers per version, request volume, 4xx, 5xx, p95 latency, pagination completion, old-SDK parse failures, and high-risk accounts that have been contacted but have not migrated.
Separate three lifecycle moments: publishing the migration policy, formally deprecating the API, and making it stop responding. RFC 9745 defines the Deprecation response header for a deprecation date and the deprecation link relation for supporting information. Add Sunset only when the provider plans for the resource to stop responding. Deprecation itself should not change resource behavior. These dates are interview examples; real dates must follow the published policy:
Deprecation: @1803859200
Sunset: Wed, 01 Sep 2027 00:00:00 GMT
Link: <https://api.example.com/migrations/orders-v2>; rel="deprecation"; type="text/html"Before retiring v1, require all of the following: support obligations are satisfied; known critical consumers have migrated or received an approved exception; remaining traffic is explained; the migration guide and support channel work; v2 error, latency, and business-result gates pass; and a shutdown rehearsal can be reversed. If a high-value customer is still blocked, extend support, provide a constrained compatibility gateway, or follow the contract. Do not ignore that customer just to display 100% adoption.
Define rollback before release. The v2 route and presentation adapter can be disabled independently, domain writes remain backward-compatible, and v1 keeps its last verified artifact. If a v2 field needs new storage, expand and backfill that storage before v2 reads it; do not delete data required by v1 in the v2 release. Rolling v2 back means restoring its implementation. Changing what “v2” means to conceal the incident would violate the contract again.
High-quality sample answer
“I would separate contract changes from the release lifecycle. These clients cannot be forced to upgrade, so I need to preserve JSON parsing, old SDK execution, and complete results with the same meaning for the same request.
Changing customer_name from a string to an object changes its type, and renaming it is a remove-plus-add operation, so both belong in v2. Paginating GET /orders by default would cause old clients to miss results, which is a semantic break and also belongs in v2. I would not assume that adding refunded is safe. If the response enum is documented as open and old SDKs preserve unknown values, v1 can expand. If generated code uses a closed enum or consumers exhaustively switch on it, I would keep the new value in v2 or first establish and test a safe unknown-value path.
I would preserve the response and all-results semantics of /v1/orders, then create /v2/orders with a structured customer object, cursor, and page rules. Both versions share querying, authorization, and order-state logic; only request parsing and response presentation differ. Before merge, I would compare OpenAPI definitions, run contract tests through the last v1 SDK, and replay sanitized requests to check status codes, ordering, defaults, and completeness. Internal SDKs and a small set of known integrations opt into v2 first. A regression disables the v2 route while v1 continues unchanged.
During migration, I would measure active consumers by application ID, use total traffic as a supporting signal, and track version adoption, parse failures, 4xx, 5xx, latency, pagination completion, and critical accounts. The guide includes field mappings, the pagination loop, enum fallback, and a test environment. Formal deprecation is discoverable through response headers and a migration link; shutdown has a separately declared date. I retire v1 only after support obligations, critical consumers, remaining traffic, and v2 SLOs all pass their gates, and after a reversible rehearsal. That makes the version identifier, compatible implementation, migration evidence, and retirement one testable plan.”
Common mistakes
- Answering only “put
/v2in the URL” → It identifies neither who breaks nor the migration and shutdown gates →
Baseline v1, then classify every change across source, wire, and semantic compatibility.
- Assuming every response addition is compatible → Strict deserializers, closed enums, and exhaustive switches can
still fail → Inspect the public contract and generated SDKs, then run real old-version tests.
- Automatically redirecting v1 to v2 → One version identifier begins to represent two semantics, so clients cannot
choose or roll back → Preserve a stable v1 representation and require explicit v2 selection.
- Copying the whole service for v1 and v2 → Security fixes and business rules drift while dual-version cost grows →
Share domain logic and isolate only the parsing and presentation that truly differ.
- Turning v1 off when the announced date arrives → Anonymous long-tail calls, low-frequency reconciliation, and
critical customers can still depend on it → **Verify adoption by consumer, obligations, and remaining traffic, then rehearse recovery.**
- Watching only server-side 2xx rates → A client can receive a response but fail to parse it, omit pages, or
misinterpret a new status → Add old-SDK results, pagination completion, end-to-end outcomes, and support signals.
Follow-up questions and responses
Follow-up 1: Do three internal consumers owned by the same company need v2?
Not necessarily. If every caller is identifiable, releases can be coordinated, and rollback is fast, use expand–migrate–contract: add a compatible field or endpoint, release consumers that can read both shapes, switch the producer, then remove the old contract after measured usage reaches zero. Contract tests and versioned deployment evidence still matter, but controllable consumers do not require a permanent public v2. One uncoordinated offline job, old client, or external partner invalidates that assumption.
Follow-up 2: Should webhook events follow the account's current API version?
Do not reinterpret historical events with the “current” version during replay. Pin an event API version when the endpoint is created, record it with the event, and preserve the original shape for retries and replay. Upgrade by creating or switching to a new-version endpoint and validating the consumer. Stripe's public documentation likewise ties webhook event shape to the API version at endpoint creation. Sending both old and new events creates duplicate side effects and is safe only as a short migration when consumers deduplicate by a stable event ID.
Follow-up 3: Is a new response enum value a breaking change?
It depends on the published contract. GitHub lists adding an enum value as additive, while Google's compatibility guidance also warns that old code may not handle a new response enum value gracefully. State that tension explicitly. If the contract defines an open set, the SDK exposes an unknown representation, and consumers must tolerate it, the change can be compatible. If the type is closed or the ecosystem contains exhaustive switches, treat it as a breaking risk: improve the SDK and contract first or put the value in a new version. A server-side schema alone cannot decide.
Follow-up 4: The unbounded v1 list is timing out. What if migration cannot finish in time?
First restore service with controls allowed by the existing contract: quotas, caching, query optimization, and backpressure, while directly moving high-volume consumers to v2. If an emergency response cap is unavoidable, state that it may break v1, use the incident and change-approval process, announce the affected scope, provide bulk export or a temporary compatibility channel, and monitor missing-data risk. Silently returning the first 100 records with a 200 response converts an availability incident into a hard-to-detect data error; it is not a compatible fix.
Follow-up 5: The shutdown date arrives, but 0.2% of traffic still uses v1. What next?
Resolve the percentage into consumers and business purpose: a probe, bad configuration, month-end job, or contracted customer. Remove identifiable traffic with no business dependency. Critical consumers need an upgrade, exception, or support escalation. Handle anonymous traffic under the published policy and risk model. Record the remaining calls, notification evidence, support obligations, recovery plan, and decision owner. A percentage alone proves neither that shutdown is safe nor that the version must live forever.