API Versioning Strategies
👉 Deep dive into Strategies and Implementation using Path param, Query Parameter, and Header Versioning in Spring Boot Microservice.
What is API Versioning ?
API versioning is a crucial strategy for managing changes to an API over time without disrupting existing clients. It’s necessary when introducing breaking changes (e.g., changing request/response formats, removing fields). A good versioning strategy ensures clients can conveniently upgrade to the latest version while clearly communicating changes.
Note
API versoning utilizes the standard Spring Web annotations to implement these HTTP architectural style versioning strategies. Hence, no external dependency is required.
Table of Contents
- What is API Versioning ?
- Table of Contents
- Resources - Old and New version
- URI Versioning
- Request Parameter Versioning
- Custom Header Versioning
- Media Type Versioning
- Comparative Analysis and Selection
- API Gateway Centerlized Versioning
- Best Practices
Resources - Old and New version
We use two versions of the Person class PersonV1 and PersonV2 representing the name of a person, to demonstrate the versioning.
Here PersonV1 represents the name as a String property while PersonV2 nests the Name class with having properties firstName & lastName representing the person name.
PersonV1.java
public class PersonV1 {
private String name;
public PersonV1(String name) {
super();
this.name = name;
}
// getter-setters omitted for brevity
}
PersonV2.java
public class PersonV2 {
private Name name;
public PersonV2(Name name) {
super();
this.name = name;
}
// getter-setters omitted for brevity
}
Name.java
public class Name {
private String firstName;
private String lastName;
public Name(String firstName, String lastName) {
super();
this.firstName = firstName;
this.lastName = lastName;
}
// getters-setters omitted for brevity
}
URI Versioning
In this method, the API version number is embedded directly in the URL path. It’s the most straightforward and common approach.
| Advantage | Drawback | Best Practice |
|---|---|---|
| Cache-friendly (versions are distinct URLs), very easy to understand and implement. | Polluting the URL and requires code changes (new controller classes/methods) for every new version, leading to potential code duplication. | Use it for major, breaking changes. Align with Semantic Versioning (e.g., /v1, /v2). |
Realworld use-case:
Twitter/X is a well-known example that uses URL versioning strategy (/v1,/v2, etc.).
Controller
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UriVersioningPersonController {
/**
* Version 1: GET /v1/person
*/
@GetMapping("/v1/person")
public PersonV1 getPersonV1() {
return new PersonV1("URI Versioning v1");
}
/**
* Version 2: GET /v2/person
*/
@GetMapping("/v2/person")
public PersonV2 getPersonV2() {
return new PersonV2(new Name("URI", "Versioning V2"));
}
}
Test with CURL command
# Version 1
curl -X GET "http://localhost:8080/v1/person"
# Version 2
curl -X GET "http://localhost:8080/v2/person"
Request Parameter Versioning
In this approach the API version is passed as a query parameter in the URL. The base URL remains constant across versions.
| Advantage | Drawback | Best Practice |
|---|---|---|
| Clean URI (base path is constant), easy to implement and test. | Polluting the URL with query parameters, less aesthetically pleasing, and not always considered RESTful as the version is not part of the resource identifier. | Best Practice: Useful when the client is a browser or when URI cleanliness is a priority, but less suitable for public-facing APIs. |
Realworld use-case:
Amazon is often cited as using this strategy for some of its services.
Controller
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RequestParamVersioningController {
/**
* Version 1: GET /person/param?version=1
*/
@GetMapping(path = "/person/param", params = "version=1")
public PersonV1 getPersonV1() {
return new PersonV1("Request Param versioning v1");
}
/**
* Version 2: GET /person/param?version=2
*/
@GetMapping(path = "/person/param", params = "version=2")
public PersonV2 getPersonV2() {
return new PersonV2(new Name("Request Parama", "Versioning v2"));
}
}
Test with CURL Command
# Version 1
curl -X GET "http://localhost:8080/person/param?version=1"
# Version 2
curl -X GET "http://localhost:8080/person/param?version=2"
Custom Header Versioning
In this strategy the version information is conveyed via a custom HTTP header, avoiding changes to the URI.
| Advantage | Drawback | Best Practice |
|---|---|---|
| Clean URLs (most RESTful in terms of URI), and versions are invisible to generic API consumers unless they inspect the headers. | Misuse of HTTP Headers (version is not standard HTTP concern), harder to test directly in a browser, and may require extra configuration for caching layers. | Use a clear header name like X-API-VERSION or Api-Version. Ensure all client libraries handle custom headers easily. |
Realworld use-case:
Microsoft (Azure APIs) has used custom headers for versioning.
Controller
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CustomHeaderVersioning {
/**
* Version 1: GET /person/header with header X-API-VERSION=1
*/
@GetMapping(path = "/person/header", headers = "X-API-VERSION=1")
public PersonV1 getPersonV1() {
return new PersonV1("Custom Header Versioning v1");
}
/**
* Version 2: GET /person/header with header X-API-VERSION=2
*/
@GetMapping(path = "/person/header", headers = "X-API-VERSION=2")
public PersonV2 getPersonV2() {
return new PersonV2(new Name("Custom Header", "Versioning v2"));
}
}
Test with CURL command
# Version 1
curl -X GET "http://localhost:8080/person/header" -H "X-API-VERSION: 1"
# Version 2
curl -X GET "http://localhost:8080/person/header" -H "X-API-VERSION: 2"
Media Type Versioning
Content Negotiation (Media Type) Versioning, also known as Accept Header Versioning. The version is specified within the Accept HTTP header using a custom media type (e.g., application/vnd.comp.app-v1+json).
| Advantage | Drawback | Best Practice |
|---|---|---|
| Most RESTful approach according to some purists, as it uses standard HTTP negotiation mechanisms. Clean URLs. | Complex to implement and can be confusing for developers. Misuse of headers, similar to custom header approach. | Follow the standard format: application/vnd.<vendor>.<resource>-v<version>+json/xml. Use it when resource representation changes fundamentally. |
Realworld use-case:
GitHub is a prominent example utilizing this strategy.
Controller
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MediaTypeVersioning {
/**
* Version 1: GET /person/accept with Accept: application/vnd.comp.app-v1+json
*/
@GetMapping(path = "/person/accept", produces = "application/vnd.comp.app-v1+json")
public PersonV1 getPersonV1() {
return new PersonV1("Mediatype Versioning v1.");
}
/**
* Version 2: GET /person/accept with Accept: application/vnd.comp.app-v2+json
*/
@GetMapping(path = "/person/accept", produces = "application/vnd.comp.app-v2+json")
public PersonV2 getPersonV2() {
return new PersonV2(new Name("Media type", "Versioning v2"));
}
}
Test with CURL Command
# Version 1
curl -X GET "http://localhost:8080/person/accept" -H "Accept: application/vnd.comp.app-v1+json"
# Version 2
curl -X GET "http://localhost:8080/person/accept" -H "Accept: application/vnd.comp.app-v2+json"
Comparative Analysis and Selection
Choosing the right approach depends on your API’s audience, update frequency, and caching needs.
| Strategy | Advantages | Drawbacks | Best For |
|---|---|---|---|
| URI | Highly visible, cache-friendly, simple. | Pollutes URL, high maintenance (code duplication). | Public APIs, major breaking changes. |
| Request Param | Clean URI base, simple to implement. | Pollutes URL (query string), less RESTful, not cache-friendly by default. | Internal APIs, simple versioning needs. |
| Custom Header | Clean URLs, version is out of the URI. | Not browser-testable, potential misuse of headers, complex caching. | APIs with frequent, minor updates. |
| Media Type | Most RESTful, clean URLs, resource-level versioning. | Most complex to implement, developer confusion, complex caching. | APIs with strong REST adherence, internal services. |
Strategy to choose right approach
The choice of API versioning strategy should be made early in the design phase and should be based on your API’s audience, caching needs, and the frequency/complexity of anticipated changes.
| Strategy | Audience | REST Purity | Caching | Complexity | Cleanliness |
|---|---|---|---|---|---|
| 🔗 URI | Public/External | Low (Pollutes URL) | High (Natively Cacheable) | Low (Simple @GetMapping) | Low (Version in URL) |
| Query Parameter | Internal/Simple APIs | Low (Uses Query Params) | Low (Requires Custom Config) | Low (Simple params attr) | Medium (Base URL clean) |
| 📧 Custom Header | Partner/Internal | Medium (Custom Header) | Medium (Requires Header-Aware Config) | Medium (Uses headers attr) | High (Cleanest URL) |
| 🎨 Media Type | Strong REST Adherence | High (Standard HTTP Negotiation) | Medium (Requires Content-Aware Config) | High (Custom Media Type String) | High (Cleanest URL) |
API Gateway Centerlized Versioning
An API Gateway acts as a single entry point for all client requests, centralizing cross-cutting concerns like security, rate limiting, and `crucially. Which make it idle component for Centerlized API Versioning and Transformation.
Gateway’s Role in Centerlizing Versioning
- Centralized Routing: The gateway inspects the incoming request (URI path, query param, or header) and routes it to the correct microservice version. This shields the client from knowing the internal microservice deployment topology.
- Decoupling: It decouples the external API contract (the version clients see) from the internal microservice implementation version.
Role in Transformation (Non-Breaking Change Scenario)
For a non-breaking change (e.g., adding an optional field in V2, like splitting name into firstName and lastName), the Gateway can simplify client migration:
- V2 Request to V1 Microservice: * If a V2 client sends a request that is backward compatible with V1, the Gateway can add default values for the new V2 fields in the request body before routing it to the V1 service.
- V1 Response Transformation: * If a V1 client calls the endpoint, the Gateway routes the request to the new V2 microservice. The V2 service responds with the new, richer V2 data structure (
firstName,lastName). The Gateway then performs a response transformation (e.g., concatenatingfirstNameandlastNameback into a singlenamefield) to match the expected V1 schema before sending it back to the V1 client.
This transformation capability allows the backend microservice to evolve faster (implementing V2) while the Gateway maintains backward compatibility for older V1 clients without requiring a full parallel V1 and V2 deployment.
sequenceDiagram
participant C as Client
participant G as API Gateway
participant S2 as Microservice V2
participant S1 as Microservice V1
C->>G: Request V1 (GET /person/header, X-API-VERSION=1)
alt Response Transformation for Non-Breaking Change
Note over G: Route to latest version S2
G->>S2: Transform Request (if needed) & Forward
S2->>G: Response V2 (with firstName, lastName)
Note over G: Transform Response (concat to name) to V1 Schema
G->>C: Response V1 (with name)
else Breaking Change
Note over G: Route to appropriate version S1
G->>S1: Forward Request V1
S1->>G: Response V1
G->>C: Response V1
end
Best Practices
- Documentation Tooling:
- Integrate an API documentation tool like OpenAPI (Swagger).
- Ensure the documentation tool clearly shows how to access each version (via URI, header, or parameter).
- Custom Annotations:
- For cleaner controller code, consider creating a custom Spring annotation (e.g.,
@ApiVersion(version = "1")) that internally handles the different versioning strategies.
- For cleaner controller code, consider creating a custom Spring annotation (e.g.,
Reference:
Check this video for deep dive into implementing various API versioning strategies - API Versioning In Spring Framework 7 by Rossen Stoyanchev @ Spring I/O 2025.