Enterprise Java

Implementing API Versioning in Spring

APIs must evolve as business requirements change, but breaking existing consumers is rarely acceptable. A clear versioning strategy enables services to introduce new behaviour while preserving backward compatibility and providing clients with a controlled upgrade path. With recent additions to Spring Framework and Spring Boot, versioned endpoints can now be implemented using built-in, consistent mechanisms instead of custom routing logic. In this article, we demonstrate how to implement API versioning in Spring using a practical example and multiple versioning strategies.

1. Why API Versioning Matters

Without versioning, every breaking change forces an immediate and coordinated update across all clients, which is rarely realistic in distributed systems. Even minor changes, such as renaming a field or altering validation rules, can cause failures in production if consumers are not prepared. Versioning reduces this risk by allowing older and newer contracts to coexist.

In addition to protecting clients, versioning improves server-side development by enabling gradual rollout of features, clearer deprecation policies, and safer refactoring. Instead of freezing APIs out of fear of breaking changes, teams can evolve them deliberately and transparently.

2. Scenario: Product API with Two Representations

In this article’s example, we expose a GET /products/{id} endpoint. Both API versions return product identity data, but they differ in how pricing is represented. This reflects a realistic evolution where the backend changes pricing logic while keeping older clients functional.

  • v1 returns the raw numeric price, and currency code
  • v2 returns a formatted display price and a discount indicator

This allows newer clients to present pricing directly while older clients continue using the original structure.

Shared Base Model

The base class contains fields that remain stable across all API versions. This allows both representations to share identity data while extending the model independently when requirements diverge.

public abstract class Product {

    private Long id;
    private String name;

    protected Product() {
    }

    protected Product(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }
}

By keeping common fields in a shared superclass, we avoid duplication and make it explicit which parts of the model are stable across versions. Each API version can now extend this base while evolving independently.

Version 1 (ProductV1) Model

The first API version exposes price information in its most basic form: a numeric value and a currency code. This design is flexible but pushes formatting responsibility to the client.

public class ProductV1 extends Product {

    private BigDecimal price;
    private String currency;

    public ProductV1() {
    }

    public ProductV1(Long id, String name, BigDecimal price, String currency) {
        super(id, name);
        this.price = price;
        this.currency = currency;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public String getCurrency() {
        return currency;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public void setCurrency(String currency) {
        this.currency = currency;
    }
}

This version is useful for clients who apply their own localization or pricing rules, but it requires additional processing before displaying prices to users.

Version 2 (ProductV2) Model

The second version shifts more responsibility to the backend by returning a preformatted price string and indicating whether a discount is applied.

public class ProductV2 extends Product {

    private String displayPrice;
    private boolean discounted;

    public ProductV2() {
    }

    public ProductV2(Long id, String name, String displayPrice, boolean discounted) {
        super(id, name);
        this.displayPrice = displayPrice;
        this.discounted = discounted;
    }

    public String getDisplayPrice() {
        return displayPrice;
    }

    public boolean isDiscounted() {
        return discounted;
    }

    public void setDisplayPrice(String displayPrice) {
        this.displayPrice = displayPrice;
    }

    public void setDiscounted(boolean discounted) {
        this.discounted = discounted;
    }
}

This model simplifies frontend logic and ensures consistent formatting across platforms, but it is not backwards compatible with the first representation, making versioning essential.

3. Java Configuration with WebMvcConfigurer

Spring supports API versioning configuration either through application properties or programmatic configuration. In this article, we use Java configuration because it offers strong typing, clearer intent, and easier customization for enterprise applications.

The configuration below shows the structure used for all examples, with each strategy enabled separately.

@Configuration
public class ApiVersionConfig implements WebMvcConfigurer {

    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {

        configurer
                // Enable ONE strategy per deployment

                // .useRequestHeader("X-API-Version")
                // .useQueryParam("version")
                // .usePathSegment(1)
                // .useMediaTypeParameter(MediaType.APPLICATION_JSON, "v")

                .addSupportedVersions("1", "2")
                .setDefaultVersion("1");
    }
}
Note
Only one versioning strategy should be active at a time.

4. Header-Based Versioning

Header-based versioning keeps URLs unchanged and passes version information through a custom HTTP header.

Configuration

.useRequestHeader("X-API-Version")

This tells Spring to resolve the API version from the X-API-Version request header and route the request accordingly.

Controller: Product API (Header Versioned)

@RestController
@RequestMapping("/products")
public class ProductHeaderController {

    @GetMapping(path = "/{id}", version = "1")
    public ProductV1 v1(@PathVariable Long id) {
        return new ProductV1(id, "Laptop", new BigDecimal("1200.00"), "USD");
    }

    @GetMapping(path = "/{id}", version = "2")
    public ProductV2 v2(@PathVariable Long id) {
        return new ProductV2(id, "Laptop", "$1,200.00", true);
    }
}

Both methods share the same URI but differ by declared API version. Spring performs version matching before invoking the controller method, so no conditional logic is needed inside the handler.

curl Test and Output

curl -i -H "X-API-Version: 1" http://localhost:8080/products/1
curl -i -H "X-API-Version: 2" http://localhost:8080/products/1
{"id":1,"name":"Laptop","price":1200.00,"currency":"USD"}
{"id":1,"name":"Laptop","displayPrice":"$1,200.00","discounted":true}

5. Query Parameter Versioning

Query parameter versioning places the version directly in the request URL.

Configuration

.useQueryParam("version")

Spring now extracts version information from the version query parameter.

Controller: Product API (Query Parameter Versioned)

@RestController
@RequestMapping("/products")
public class ProductQueryController {

    @GetMapping(path = "/{id}", params = "version=1")
    public ProductV1 v1(@PathVariable Long id, @RequestParam String version) {
        return new ProductV1(id, "Phone", new BigDecimal("800.00"), "USD");
    }

    @GetMapping(path = "/{id}", params = "version=2")
    public ProductV2 v2(@PathVariable Long id, @RequestParam String version) {
        return new ProductV2(id, "Phone", "$800.00", false);
    }
}

This controller uses the same path as the header-based version but relies on query parameters to differentiate behaviour.

curl Test

curl -i "http://localhost:8080/products/2?version=1"
curl -i "http://localhost:8080/products/2?version=2"

6. Media Type Versioning

Media type versioning relies on HTTP content negotiation, embedding the version inside the Accept header. This strategy aligns well with REST principles and allows format evolution alongside API changes.

Configuration

.useMediaTypeParameter(MediaType.APPLICATION_JSON, "v")

Spring extracts the version from the media type parameter.

Controller: Product API (Media Type Versioned)

@RestController
@RequestMapping("/products")
public class ProductMediaController {

    @GetMapping(
            path = "/{id}",
            produces = "application/json;v=1",
            version = "1"
    )
    public ProductV1 v1(@PathVariable Long id) {
        return new ProductV1(id, "Monitor", new BigDecimal("300.00"), "USD");
    }

    @GetMapping(
            path = "/{id}",
            produces = "application/json;v=2",
            version = "2"
    )
    public ProductV2 v2(@PathVariable Long id) {
        return new ProductV2(id, "Monitor", "$300.00", true);
    }
}

Each method explicitly declares the media type it produces, ensuring accurate routing based on content negotiation.

curl Test

curl -i -H "Accept: application/json;v=1" http://localhost:8080/products/4
curl -i -H "Accept: application/json;v=2" http://localhost:8080/products/4

7. Path Segment Versioning

Path-based versioning embeds the version into the URL structure, making it highly visible in logs and documentation. This approach should be used for public APIs where discoverability is important.

.usePathSegment(1)

Spring reads the version from the first path segment after the root.

Controller: Product API (Path Versioned)

@RestController
@RequestMapping("/products")
public class ProductPathController {

    @GetMapping(value = "/{version}/{id}", version = "1")
    public ProductV1 v1(@PathVariable Long id) {
        return new ProductV1(id, "Tablet", new BigDecimal("500.00"), "USD");
    }

    @GetMapping(value = "/{version}/{id}", version = "2")
    public ProductV2 v2(@PathVariable Long id) {
        return new ProductV2(id, "Tablet", "$500.00", false);
    }
}

The version placeholder appears in the path, while Spring still validates the version against the configured strategy.

curl Test

curl -i http://localhost:8080/products/v1/3
curl -i http://localhost:8080/products/v2/3

8. Conclusion

In this article, we showed how API versioning in Spring can be applied in a structured and maintainable way using a realistic Product API scenario, keeping shared data in common models while isolating changes in version-specific representations. The introduction of first-class API versioning in Spring Framework 7 and Spring Boot 4 represents a major step forward for API design in the Spring ecosystem.

With versioning now integrated directly into the framework, Spring provides a consistent and developer-friendly approach to evolving APIs safely. Whether we are maintaining existing services or building modern microservices, using these built-in capabilities helps us introduce change without disruption while guiding clients along a smoother and more predictable upgrade path.

9. Download the Source Code

This article explored API versioning in Spring.

Download
You can download the full source code of this example here: spring api versioning

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button