When a public-facing service needs to expose multiple versions of its API at once (e.g., /v1/orders vs /v2/orders, or via an Accept header), what are the main versioning strategies, and what's the operational cost of each?
answer
- URI path vs header vs content-negotiation vs query param
- URI = visible/simple but duplicates route trees
- header = clean URIs but harder to route/debug
- query param = poor caching behavior
- cost is running N live versions regardless of mechanism
basics
~20 sYou can put the version in the URL path, in a request header, or in a query parameter. URL versioning is the simplest and most visible; header-based versioning keeps URLs stable but is less discoverable. All of them mean you're running and maintaining more than one version of your API at once.
solid answer
~50 sThe three common strategies are URI/path versioning (/v1/orders, /v2/orders), custom header or media-type versioning (Accept: application/vnd.company.orders.v2+json, or a dedicated X-API-Version header), and query-parameter versioning (?version=2), which is less common and considered poor practice since caches and proxies don't treat query params as cache-key-significant by default. URI versioning is the most operationally simple and most visible/debuggable - you can literally see the version in logs, curl commands, and browser URLs - but it clutters resource identity and tempts teams to duplicate whole route trees. Header/content-negotiation versioning keeps a single canonical URI per resource, which is more RESTfully 'correct', but is harder to test manually and less discoverable to integrators reading logs. Regardless of the mechanism, the deeper operational cost is the same: every additional live major version is another code path to test, monitor, and eventually deprecate - the versioning scheme just decides how consumers select among them, not whether you pay that cost.
go deeper
Can name that APIs sometimes have a v1/v2 in the URL and give a basic reason (breaking change happened).
Can name at least two mechanisms (URI, header) and describe the basic trade-off between visibility and clean resource identity.
Explains the caching pitfall of query params, the gateway-routing trade-off of header versioning, and insists the real cost is maintaining N live versions regardless of mechanism.
Sets org-wide versioning policy (e.g., standardizing on one mechanism across all services), mandates usage telemetry for deprecation decisions, and evaluates strategies like Stripe's dated-version-with-shims model against the cost of N fully-duplicated code paths.
## When a provider needs more than one live version When a provider needs multiple, simultaneously-live major versions of an API - because it made a genuinely breaking change and can't force every consumer to migrate atomically - it needs a mechanism for a given request to declare which version's contract it expects. ## The three mechanisms There are three commonly used mechanisms. | Mechanism | How a request declares its version | |---|---| | **URI/path versioning** | embeds the version directly in the resource path, e.g. `GET /v1/orders/42` versus `GET /v2/orders/42` | | **Header or media-type versioning** | keeps a single canonical URI (`GET /orders/42`) and lets the client declare the desired version via a request header, either a custom header like `X-API-Version: 2` or, more RESTfully, via content negotiation using a custom media type in the `Accept` header, e.g. `Accept: application/vnd.acme.orders.v2+json` | | **Query-parameter versioning** | appends the version as a query string, e.g. `GET /orders/42?version=2` | Query-parameter versioning is the least favored of the three because many HTTP caches, CDNs, and proxies don't treat query parameters as cache-key-significant by default, so version-2 responses can get cached and served under version-1 URLs (or vice versa) unless the caching layer is specifically configured to include the parameter in its cache key. ## Why more than one strategy exists The reason multiple strategies exist at all is that they trade off differently along several axes: visibility/debuggability, resource-identity purity, and infrastructure compatibility. - **URI versioning wins on visibility** - anyone reading an access log, a curl command, or a browser address bar can immediately see which version was called, which is enormously useful for debugging and for API gateways/CDNs that want to route or cache by version using simple path-prefix rules. Its downside is conceptual: from a strict REST standpoint, `/v1/orders/42` and `/v2/orders/42` arguably represent the same resource at different representations, and putting the version in the path makes the URI itself version-dependent, which duplicates route trees and tempts implementers into copy-pasting entire controllers between v1 and v2 packages rather than sharing logic cleanly. - **Header/content-negotiation versioning keeps resource identity clean** - one URI, many representations - which is architecturally tidier, but it's harder for a human to debug (you can't just paste a URL into a browser and see which version you're hitting), and simple path-based routing at a gateway or CDN no longer works, since the routing decision now requires inspecting headers, which not every piece of infrastructure supports equally well. ## The trade-off that matters most The trade-off that matters most in practice, though, is one that's identical no matter which mechanism you choose: the real operational cost of API versioning isn't the labeling scheme, it's that you are now running and maintaining **N live major versions of business logic simultaneously**: - more code paths to test; - more monitoring dashboards and alerts to duplicate; - more security patches to backport; - more surface area for a bug to hide in a rarely-exercised old version. Teams that pick a 'clean' versioning mechanism but never actually retire old versions accumulate this cost indefinitely; the mechanism doesn't solve that problem, only a deliberate deprecation policy does. ## Failure modes Failure modes cluster around two things: caching and abandoned versions. - **Caching**: the query-parameter caching failure described above is a classic production incident - a CDN configured to cache by path only serves a v1 response to a v2 request (or the reverse) because the version query parameter wasn't included in the cache key, producing intermittent, hard-to-reproduce 'wrong version of data' bug reports. - **Abandoned versions**: the abandoned-version failure is organizational - a provider ships v2, most consumers migrate, but two or three low-priority integrations never do, and years later the team wants to remove v1's code path but can't, because nobody tracked which consumers are still calling it. This is why serious versioning strategies pair the mechanism with usage telemetry from day one, so a team can prove a version's usage has dropped to zero before deleting it, rather than guessing. ## What this looks like in practice A well-known real-world example is Stripe's approach: Stripe uses a dated-version header-negotiation mechanism (accounts are pinned to a dated API version, and requests can override it via a `Stripe-Version` header) rather than URI versioning, explicitly to keep URIs stable while still supporting many simultaneously-live historical contract shapes, internally implemented via request/response transformation shims between versions rather than maintaining fully separate code paths per version - a deliberate design choice to control the 'N live versions' cost described above.
- Why is query-parameter versioning generally discouraged even though it's the simplest to implement?Many HTTP caches, CDNs, and reverse proxies don't include query parameters in their cache key by default, so a cached response for one version can be served to a request asking for a different version, producing intermittent and hard-to-diagnose bugs. It also doesn't solve the underlying resource-identity question any more cleanly than URI versioning does, so it inherits most of URI versioning's downsides without its debuggability upside.
- What organizational practice is needed to actually retire an old API version, regardless of which versioning mechanism is used?Usage telemetry tagged by version, tracked from the moment a new version ships, so the team can see traffic to the old version trend toward zero and identify exactly which consumers are still calling it before removing the code path. Without this, teams either keep old versions alive indefinitely out of fear, or remove them and break a consumer nobody knew still depended on it.
- Why does header/content-negotiation versioning make gateway and CDN routing harder compared to URI versioning?Path-based routing rules at a gateway or CDN can route purely on the URL prefix without inspecting the request body or headers, which is fast and universally supported. Header-based version selection requires the routing layer to parse a custom header or Accept media type, which not all infrastructure, especially simpler CDNs or load balancers, supports as flexibly as path matching.
Like choosing how a restaurant marks its two different menus - a separate printed booklet per menu (URL path) or a note the waiter asks for (header) or a sticky note on one shared menu (query param) - the real cost isn't the labeling scheme, it's that the kitchen has to keep cooking two different sets of dishes either way.
saying these in an interview costs you the question
- thinks choosing a versioning mechanism eliminates the cost of running multiple versions
- recommends query-parameter versioning without mentioning the caching pitfall
- can't name at least two of the three common mechanisms
- has no answer for how/when to retire an old version
- believes header-based versioning is strictly superior with no operational downside