SoFunction
Updated on 2025-04-14

Summary of 6 API version control strategies in SpringBoot

API version control is a key strategy to ensure the smooth evolution of the system. When the API changes, a reasonable version control mechanism can allow the old client to continue to work normally, while allowing the new client to use new features.

1. URL path version control

This is the most intuitive and widely used version control method, by directly including the version number in the URL path.

Implementation method

@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
    
    @GetMapping("/{id}")
    public UserV1DTO getUser(@PathVariable Long id) {
        // Return the user information of v1 version        return userService.getUserV1(id);
    }
}

@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
    
    @GetMapping("/{id}")
    public UserV2DTO getUser(@PathVariable Long id) {
        // Returns the user information of v2 version, which may contain more fields        return userService.getUserV2(id);
    }
}

Pros and cons

advantage

  • Simple and intuitive, clear client calls
  • Completely isolate different versions of the API
  • Easy API gateway routing and document management

shortcoming

  • May cause code duplication
  • Maintain multiple versions of the controller class

2. Request parameter version control

Keep the URL path unchanged by specifying the version number in the request parameter.

Implementation method

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping("/{id}")
    public Object getUser(@PathVariable Long id, @RequestParam(defaultValue = "1") int version) {
        switch (version) {
            case 1:
                return userService.getUserV1(id);
            case 2:
                return userService.getUserV2(id);
            default:
                throw new IllegalArgumentException("Unsupported API version: " + version);
        }
    }
}

Or use SpringMVC's conditional mapping:

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping(value = "/{id}", params = "version=1")
    public UserV1DTO getUserV1(@PathVariable Long id) {
        return userService.getUserV1(id);
    }
    
    @GetMapping(value = "/{id}", params = "version=2")
    public UserV2DTO getUserV2(@PathVariable Long id) {
        return userService.getUserV2(id);
    }
}

Pros and cons

advantage

  • Maintain the semantics of URL resource positioning
  • Relatively simple to implement
  • The client can easily switch versions by querying parameters

shortcoming

  • May be confused with business query parameters
  • Inconvenient to cache (same URL different versions)
  • Not as obvious as the URL path version

3. HTTP Header version control

This is a more RESTful way to specify the API version by customizing HTTP headers.

Implementation method

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping(value = "/{id}", headers = "X-API-Version=1")
    public UserV1DTO getUserV1(@PathVariable Long id) {
        return userService.getUserV1(id);
    }
    
    @GetMapping(value = "/{id}", headers = "X-API-Version=2")
    public UserV2DTO getUserV2(@PathVariable Long id) {
        return userService.getUserV2(id);
    }
}

Pros and cons

advantage

  • Keep the URL clean and in line with RESTful concept
  • Completely separate version information from business parameters
  • Can carry more information about the version

shortcoming

  • Not easy to test in the browser
  • Higher requirements for API documentation
  • The client needs special processing header information

4. Accept Header version control (media type version control)

Using the content negotiation mechanism of the HTTP protocol, specify the media type and its version through the Accept header.

Implementation method

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping(value = "/{id}", produces = "application/-v1+json")
    public UserV1DTO getUserV1(@PathVariable Long id) {
        return userService.getUserV1(id);
    }
    
    @GetMapping(value = "/{id}", produces = "application/-v2+json")
    public UserV2DTO getUserV2(@PathVariable Long id) {
        return userService.getUserV2(id);
    }
}

The Accept header needs to be set when requesting by the client:

Accept: application/-v2+json

Pros and cons

advantage

  • Most compliant with HTTP specifications
  • Using the existing mechanism of content negotiation
  • Keep URLs clean and semantic

shortcoming

  • The threshold for client usage is high
  • Not intuitive, inconvenient to debug
  • Custom MediaType parsing may be required

5. Custom annotation version control

More flexible version control with custom annotations and interceptors/filters.

Implementation method

First define the version annotation:

@Target({, })
@Retention()
public @interface ApiVersion {
    int value() default 1;
}

Create a version matching request mapping processor:

@Component
public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    @Override
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        ApiVersion apiVersion = ();
        return createCondition(apiVersion);
    }

    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        ApiVersion apiVersion = ();
        return createCondition(apiVersion);
    }

    private ApiVersionCondition createCondition(ApiVersion apiVersion) {
        return apiVersion == null ? new ApiVersionCondition(1) : new ApiVersionCondition(());
    }
}

public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {

    private final int apiVersion;

    public ApiVersionCondition(int apiVersion) {
         = apiVersion;
    }

    @Override
    public ApiVersionCondition combine(ApiVersionCondition other) {
        // Use the highest version        return new ApiVersionCondition((, ));
    }

    @Override
    public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
        String version = ("X-API-Version");
        if (version == null) {
            version = ("version");
        }
        
        int requestedVersion = version == null ? 1 : (version);
        return requestedVersion >= apiVersion ? this : null;
    }

    @Override
    public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
        // Preferentially match higher versions        return  - ;
    }
}

Configure WebMvc to use a custom mapping processor:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping();
    }
}

Use custom annotations:

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @ApiVersion(1)
    @GetMapping("/{id}")
    public UserV1DTO getUserV1(@PathVariable Long id) {
        return userService.getUserV1(id);
    }
    
    @ApiVersion(2)
    @GetMapping("/{id}")
    public UserV2DTO getUserV2(@PathVariable Long id) {
        return userService.getUserV2(id);
    }
}

Pros and cons

advantage

  • Highly flexible and customizable
  • Can combine multiple version control strategies
  • The code organization is clearer

shortcoming

  • More complex implementation
  • Need to customize Spring components

6. Interface-oriented API version control

The core idea is to provide different version implementation classes with the same interface.

Implementation method

First define the API interface:

public interface UserApi {
    Object getUser(Long id);
}

@Service
@Primary
public class UserApiV2Impl implements UserApi {
    // The latest version is implemented    @Override
    public UserV2DTO getUser(Long id) {
        // Return V2 version data        return new UserV2DTO();
    }
}

@Service
@Qualifier("v1")
public class UserApiV1Impl implements UserApi {
    // Old version implementation    @Override
    public UserV1DTO getUser(Long id) {
        // Return V1 version data        return new UserV1DTO();
    }
}

The controller layer dynamically selects implementation based on version:

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    private final Map<Integer, UserApi> apiVersions;
    
    // Collect all implementations through construct injection    public UserController(List<UserApi> apis) {
        // Simplify the example, you should actually mark the version of each implementation in some way         = (
            1, ().filter(api -> api instanceof UserApiV1Impl).findFirst().orElseThrow(),
            2, ().filter(api -> api instanceof UserApiV2Impl).findFirst().orElseThrow()
        );
    }
    
    @GetMapping("/{id}")
    public Object getUser(@PathVariable Long id, @RequestParam(defaultValue = "2") int version) {
        UserApi api = (version, (2)); // Use the latest version by default        return (id);
    }
}

You can implement a version delegator yourself to simplify version selection:

// Custom API version delegatorpublic class ApiVersionDelegator<T> {
    
    private final Class<T> apiInterface;
    private final Map<String, T> versionedImpls = new HashMap<>();
    private final Function<HttpServletRequest, String> versionExtractor;
    private final String defaultVersion;
    
    public ApiVersionDelegator(Class<T> apiInterface, 
                          Function<HttpServletRequest, String> versionExtractor,
                          String defaultVersion,
                          ApplicationContext context) {
         = apiInterface;
         = versionExtractor;
         = defaultVersion;
        
        // Find all beans that implement this interface from Spring context        Map<String, T> impls = (apiInterface);
        for (<String, T> entry : ()) {
            ApiVersion apiVersion = ().getClass().getAnnotation();
            if (apiVersion != null) {
                ((()), ());
            }
        }
    }
    
    public T getApi(HttpServletRequest request) {
        String version = (request);
        return (version, (defaultVersion));
    }
    
    // Builder pattern simplifies the creation process    public static <T> Builder<T> builder() {
        return new Builder<>();
    }
    
    public static class Builder<T> {
        private Class<T> apiInterface;
        private Function<HttpServletRequest, String> versionExtractor;
        private String defaultVersion;
        private ApplicationContext applicationContext;
        
        public Builder<T> apiInterface(Class<T> apiInterface) {
             = apiInterface;
            return this;
        }
        
        public Builder<T> versionExtractor(Function<HttpServletRequest, String> versionExtractor) {
             = versionExtractor;
            return this;
        }
        
        public Builder<T> defaultVersion(String defaultVersion) {
             = defaultVersion;
            return this;
        }
        
        public Builder<T> applicationContext(ApplicationContext applicationContext) {
             = applicationContext;
            return this;
        }
        
        public ApiVersionDelegator<T> build() {
            return new ApiVersionDelegator<>(apiInterface, versionExtractor, defaultVersion, applicationContext);
        }
    }
}

Configuring and using the delegator:

@Configuration
public class ApiConfiguration {
    
    @Bean
    public ApiVersionDelegator<UserApi> userApiDelegator(ApplicationContext context) {
        return ApiVersionDelegator.<UserApi>builder()
            .apiInterface()
            .versionExtractor(request -> {
                String version = ("X-API-Version");
                return version == null ? "2" : version;
            })
            .defaultVersion("2")
            .applicationContext(context)
            .build();
    }
}

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    private final ApiVersionDelegator<UserApi> apiDelegator;
    
    public UserController(ApiVersionDelegator<UserApi> apiDelegator) {
         = apiDelegator;
    }
    
    @GetMapping("/{id}")
    public Object getUser(@PathVariable Long id, HttpServletRequest request) {
        UserApi api = (request);
        return (id);
    }
}

Pros and cons

advantage

  • Realize the separation of concerns
  • Follow the principle of opening and closing, the new version only needs to add a new implementation
  • Decoupling of business logic and version control

shortcoming

  • A good interface hierarchy is required
  • Additional adaptation layers may be required to deal with return type differences
  • Initial setup is more complicated

7. Summary

The above 6 API version control methods have their own advantages and disadvantages. The following factors should be considered when choosing.

  • Project size and team situation: Small projects can choose simple URL path versioning, while large projects can consider custom annotations or interface-oriented approaches
  • Client Type: Browser-oriented APIs may be more suitable for URL paths or query parameter versioning, while APIs for mobile applications or other services may consider HTTP header or media type versioning
  • Version evolution strategy: Whether backward compatibility is required, how often the version update is
  • API Gateway and Documentation: Consider whether the version control method facilitates API gateway routing and document generation

Finally, version control is just a means, not an end. The key is to build an evolving API architecture so that the system can continuously meet changes in business needs. Choosing the appropriate version control strategy can achieve smooth evolution of the API while ensuring system stability.

The above is the detailed content of the summary of the 6 API version control strategies in SpringBoot. For more information about SpringBoot API version control, please follow my other related articles!