[MM’s] Boot Notes — Mastering API Evolution: How Spring 7 Finally Solves Versioning
From Custom Hacks to First-Class Support: A Look at the New Declarative Approach.

Maintaining a production API presents a common dilemma: as business needs evolve, new features are introduced, and data models change, how can these changes be implemented without disrupting the applications that rely on the existing contract? For years, Spring developers have employed a range of custom solutions to address this challenge, including path prefixes, special headers, and intricate content negotiation rules. While these workarounds provide a temporary solution, they often result in boilerplate code, inconsistencies, and maintenance difficulties.
That’s all about to change. With its upcoming release, Spring Framework 7 introduces native, first-class support for API versioning, transforming a once-manual chore into a declarative, elegant part of the framework.
⏩ TL;DR (Quick Recap)
- Native, First-Class Support: Eliminating the need for custom workarounds like URL rewriting or manual header checks.
- Declarative and Simple: Versioning is now a declarative part of your controller mappings, using a new version attribute in @RequestMapping and its variants
- Flexible Strategies: Out-of-the-box support for the most common versioning strategies: request header, path segment, query parameter, and media type.
- End-to-End Integration: The feature is fully integrated across the Spring ecosystem, including client-side support (RestClient, WebClient) and testing utilities (WebTestClient, MockMvc).
- Graceful Evolution: Features like “baseline versions” (e.g., version = "1.2+") and built-in deprecation hints allow for incremental and backward-compatible API evolution.
A New, Declarative Approach
At the heart of the new feature is the version attribute, now available on @RequestMapping and its shortcuts like @GetMapping and @PostMapping. This allows you to bind a controller method directly to a specific API version.
No more messy path manipulations or separate controller classes for each version. A single controller can now cleanly handle multiple API versions.
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping(path = "/{id}", version = "1.1+")
public Product getProductV1(@PathVariable String id) {
return new Product(id, "Coffee Maker", 49.99, true);
}
@GetMapping(path = "/{id}", version = "2.0")
public ProductV2 getProductV2(@PathVariable String id) {
return new ProductV2(id, "Coffee Maker", 4999, "USD", "IN_STOCK");
}
}
Notice the version = "1.1+" syntax. This is a baseline version, which matches version 1.1 and any subsequent, higher versions that don't have a more specific mapping. This is incredibly useful for making incremental, backward-compatible changes. The getProductV1 method will continue to serve versions 1.2, 1.3, and so on, until you decide to introduce a new breaking change with a more specific mapping like version = "2.0".
Configuring Your Versioning Strategy
Spring’s new feature is unopinionated about how a client specifies the version, giving you full control. You can configure one of four common strategies by implementing WebMvcConfigurer (or WebFluxConfigurer for reactive applications).
- Request Header: Use a custom header like API-Version. This keeps your URLs clean.
- Path Segment: Embed the version in the URL, like /api/v1.1/orders.
- Query Parameter: Add a parameter to the URL, like /orders?api-version=1.1.
- Media Type: Use a parameter in the Accept header, like Accept: application/json;version=1.1.
Configuration is straightforward. To use a request header, for instance, you just add the following configuration:
@Configuration
public class ApiVersionConfig implements WebMvcConfigurer {
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
configurer.useRequestHeader("API-Version");
}
}
For Spring Boot applications, this is even simpler with a property in application.properties:
spring.mvc.apiversion.use.header=API-Version
End-to-End Support: Clients and Testing
A versioning strategy is only as good as its tooling. Spring’s new feature provides seamless integration for both the client and testing layers, ensuring a consistent experience from development to consumption.
Client-Side Integration
When using RestClient or WebClient, you can configure an ApiVersionInserter once and then simply specify the version for each request. This decouples the "how" from the "what"—your client code requests a version without needing to know if it's being sent via a header, path, or query parameter.
// Configure the client once
var client = RestClient.builder()
.baseUrl("http://localhost:8080")
.apiVersionInserter(ApiVersionInserter.useHeader("API-Version"))
.build();
var order = client.get()
.uri("/orders/123")
.apiVersion("1.1") // Simple and clean
.retrieve()
.body(OrderV2.class);
Simplified Testing
This integration extends naturally to WebTestClientor RestTestClient (new in Spring 7.0) You can configure the ApiVersionInserter in your test setup and then write clean, readable tests that verify the behavior of your versioned endpoints.
@SpringBootTest
@AutoConfigureMockMvc
class ProductControllerTest {
@Autowired
private WebTestClient webTestClient;
@Test
void testGetProductVersion1_0() {
webTestClient
.get()
.uri("/api/products/456")
.apiVersion(1.1)
.exchange()
.expectStatus()
.isOk()
.expectBody(Product.class)
.value(product -> {
assertThat(product.id()).isEqualTo("456");
assertThat(product.title()).isEqualTo("Coffee Maker");
assertThat(product.priceUsd()).isEqualTo(49.99);
assertThat(product.isAvailable()).isTrue();
});
}
@TestConfiguration
static class TestConfig implements WebTestClientBuilderCustomizer {
@Override
public void customize(WebTestClient.Builder builder) {
builder.apiVersionInserter(ApiVersionInserter.useHeader("X-API-Version"));
}
}
}
A New Era for API Management in Spring
Spring Framework 7 introduces native API versioning, a foundational feature designed to help developers manage the API lifecycle with confidence. This declarative and fully integrated solution allows APIs to evolve gracefully, resulting in more robust and maintainable services.
💡Key advantages include:
- Reduced Complexity: Replace custom filters and complex routing with a single annotation, making your code cleaner and more focused on business logic.
- Future-Proofing: Evolve your APIs without breaking existing clients by cleanly managing multiple versions within your controllers.
- Improved Maintainability: Adopt a standardized, framework-supported approach for versioning across all projects, leading to more predictable services.
You can find all the code on GitHub.
Originally posted on marconak-matej.medium.com.