API Versioning in Spring Boot 4

“Our brains are like bonsai trees, growing around our private versions of reality.”

Sloane Crosley

As a software developer, I’ve often faced the challenge of versioning APIs. Spring Boot 4 has brought significant changes in this area, and in this post I’d like to introduce you to these developments and the new possibilities they offer.

API versioning is a central aspect of developing and maintaining REST APIs. It allows existing interfaces to be further developed without breaking compatibility with existing clients. In practice, this means: New features can be added, existing functions can be adapted, and outdated endpoints can be phased out—all without requiring immediate changes to existing applications.

The challenge often lies in choosing the right versioning strategy. Should the version be included in the URL (/api/v1/users), transmitted in the HTTP header, or are there other approaches? Each method has its pros and cons in terms of readability, caching behavior, and maintainability.

Previous approaches in Spring Boot

Before we look at the new features in Spring Boot 4, let’s take a look at the best practices we’ve been using so far:

1. URL-Path Versioning

The traditional approach was to use the URL path for versioning:

@RestController
@RequestMapping("/api")
public class AncestorController {

    @GetMapping("v1/ancestors")
    public List<AncestorDto> getAncestorsV1() {
        // V1 Implementierung
    }

    @GetMapping("v2/ancestors")
    public List<AncestorDto> getAncestorsV2() {
        // V2 Implementierung
    }
}

Pros: Easy to understand, easy to test, clearly visible
Cons: Code duplication

2. Request Parameter Versioning

Another option was versioning using query parameters:

@RestController
@RequestMapping("/api/ancestors")
public class AncestorController {

    @GetMapping(params = "version=1")
    public List<AncestorDto> getAncestorsV1() {
        // V1 Implementierung
    }

    @GetMapping(params = "version=2")
    public List<AncestorDto> getAncestorsV2() {
        // V2 Implementierung
    }
}

Pros: Flexible, URL remains the same
Cons: Less intuitive; may get lost in the documentation

3. Header Versioning

Versioning using custom headers:

@RestController
@RequestMapping("/api/ancestors")
public class AncestorController {

    @GetMapping(headers = "X-ANCESTOR-API-VERSION=1")
    public List<AncestorDto> getAncestorsV1() {
        // V1 Implementierung
    }

    @GetMapping(headers = "X-ANCESTOR-API-VERSION=2")
    public List<AncestorDto> getAncestorsV2() {
        // V2 Implementierung
    }
}

Pros: The URL remains clean, RESTful
Cons: Harder to test, not immediately visible

4. Media Type Versioning (Content Negotiation)

@RestController
@RequestMapping("/api/ancestors")
public class AncestorController {

    @GetMapping(produces = "application/vnd.ancestors.v1+json")
    public List<AncestorDto> getAncestorsV1() {
        // V1 Implementierung
    }

    @GetMapping(produces = "application/vnd.ancestors.v2+json")
    public List<AncestorDto> getAncestorsV2() {
        // V2 Implementierung
    }
}

Pros: Very RESTful, follows HTTP standards
Cons: Complex, difficult to document

The new features in Spring Boot 4

With Spring Boot 4 (based on Spring Framework 7.0+), API versioning has been significantly simplified and standardized. The new version annotation attributes and the built-in versioning framework make life much easier.

1. The new version Annotation Attributes

Spring Boot 4 introduces native support for API versioning:

@RestController
@RequestMapping("/api")
public class AncestorController {

    @GetMapping("{version}/ancestors", version="1.0")
    public List<AncestorDto> getAncestorsV1() {
        // V1 Implementierung
    }

    @GetMapping("{version}/ancestors", version="1.0")
    public List<AncestorDto> getAncestorsV2() {
        // V2 Implementierung
    }
}

2. Configurable versioning strategies

We can now configure the versioning strategy centrally in application.yml or application.properties:

spring.mvc.apiversion.default=1.0
spring.mvc.apiversion.supported=1.3,1.4
spring.mvc.apiversion.detect-supported=true
#spring.mvc.apiversion.use.path-segment=0          # (/1.0/users)
#spring.mvc.apiversion.use.header=X-API-Version    # (X-API-Version: 1.0)
#spring.mvc.apiversion.use.query-parameter=version # (?version=1.0)
#spring.mvc.apiversion.use.media-type-parameter[application/json]=version # (application/json;version=1.0)

Setting spring.mvc.apiversion.default=1.0 specifies the default version for requests. If no version is found, this version is used. Setting spring.mvc.apiversion.supported=1.3,1.4 allows you to specify the versions that are supported. The reason only 1.3 and 1.4 are listed here is due to the next parameter, spring.mvc.apiversion.detect-supported=true. This allows additional versions to be determined via the controllers.

The four properties beginning with spring.mvc.apiversion.use enable versioning via the URI, the headers, a query parameter, or the media type.

3. Automatic routing

Spring Boot 4 now automatically handles routing based on the selected strategy. In this example, the controller uses the property spring.mvc.apiversion.use.path-segment=1. With this setting the second path segment from the URL is used to determine the version.

@RestController
@RequestMapping("/api/{version}")
public class AncestorController {
    private final AncestorService service;

    public AncestorController(AncestorService service) {
      this.service = service
    }

    @GetMapping("ancestors")
    public List<Ancestor> getAncestors1_0() {
        return service.findAll1_0();
    }

    @GetMapping(path = "ancestors", version = "1.1")
    public List<Ancestor> getAncestors1_1() {
        return service.findAll1_1();
    }

    @GetMapping(path= "ancestors", version = "1.2+")
    public List<Ancestor> getAncestors1_X(@PathVariable String version) {
        return service.findAll1_X(version);
    }

    @GetMapping(value = "ancestors", version = "1.5")
    public List<Ancestor> getAncestors1_5() {
        return service.findAll1_5();
    }
}

The four controller methods are used for a wide variety of URIs. For the path /api/1.0/ancestors, the getAncestors1_0 method is used because the other methods only support higher versions. For the path /api/1.4/ancestors, the getAncestors1_X method is used because 1.2+ encompasses versions 1.2, 1.3, and 1.4. The @PathVariable String version can be used in this method to check which of the three versions was actually called.

By the way, the standard version supports semantic versioning with prefixes. Instead of 1.0, you can also use 1, 1.0.0, v1.0, and even JK1.0.0.

The next example uses the property spring.mvc.apiversion.use.header=X-Ancestor-API-Version. This determines the version via the request header X-Ancestor-API-Version.

@RestController
@RequestMapping("/api")
public class AncestorController {
    private final AncestorService service;

    public AncestorController(AncestorService service) {
      this.service = service
    }

    @GetMapping("ancestors")
    public List<Ancestor> getAncestors1_0() {
        return service.findAll1_0();
    }

    @GetMapping(path = "ancestors", version = "1.1")
    public List<Ancestor> getAncestors1_1() {
        return service.findAll1_1();
    }

    @GetMapping(path= "ancestors", version = "1.2+")
    public List<Ancestor> getAncestors1_X(@RequestHeader("X-Ancestor-API-Version") String version) {
        return service.findAll1_X(version);
    }

    @GetMapping(value = "ancestors", version = "1.5")
    public List<Ancestor> getAncestors1_5() {
        return service.findAll1_5();
    }
}

The four controller methods are used for a wide variety of headers. For the header X-Ancestor-API-Version: 1.0, the getAncestors1_0 method is used because the other methods only support higher versions. For the X-Ancestor-API-Version: 1.4 header, the getAncestors1_X.Der Grund ist der selbe wie im vorherigen Beispiel. Using the @RequestHeader("X-Ancestor-API-Version") string version, this method can determine which of the three versions was actually called.

For the other strategies, the examples look identical, apart from the additional parameter for passing the actual version. This demonstrates another advantage of API versioning in Spring Boot 4. Thanks to the separation of concerns, the controller implementation is agnostic with regard to the chosen strategy. In fact, you could still make adjustments to the versioning strategy of a REST interface without changing any code, even when it’s actually already far too late.

5. Deprecation and Sunset Support

Some APIs become obsolete and should be phased out, and it’s important to announce this well in advance via the API. Spring Boot 4 offers built-in support for API deprecation:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        StandardApiVersionDeprecationHandler deprecationHandler = new StandardApiVersionDeprecationHandler();
        StandardApiVersionDeprecationHandler.VersionSpec v1 = deprecationHandler.configureVersion("1.0");
        v1.setSunsetDate(LocalDate.of(2026, Month.APRIL, 1).atStartOfDay(ZoneOffset.UTC));
        StandardApiVersionDeprecationHandler.VersionSpec v1_1 = deprecationHandler.configureVersion("1.1");
        v1_1.setDeprecationDate(LocalDate.of(2026, Month.APRIL, 1).atStartOfDay(ZoneOffset.UTC).plusMonths(6));
        v1_1.setSunsetDate(LocalDate.of(2026, Month.APRIL, 1).atStartOfDay(ZoneOffset.UTC).plusYears(1));
        configurer.setDeprecationHandler(deprecationHandler);
    }
}

The DeprecationDate is the date from which this version is deprecated, and the SunsetDate is the date from which the API is scheduled to be shut down. For requests using version 1.1, this mechanism automatically adds the following HTTP headers, in accordance with RFC 9745 and RFC 8594:

Deprecation: @1790812800
Sunset: Thu, 1 Apr 2027 00:00:00 GMT

Fazit

Spring Boot 4 finally offers a standardized and elegant solution for API versioning. New features such as version annotation attributes, configurable strategies, and built-in deprecation support make life as a developer significantly easier.

My recommendation: When you start a new project or modernize an existing API, take advantage of the new capabilities offered by Spring Boot 4. Your API consumers—and your future self—will thank you for it!

Leave a Comment