Spring Boot 4 Reduces API Versioning Code from Hundreds to Three Lines
The article explains how Spring Boot 4 introduces a built‑in ApiVersionStrategy that replaces custom interceptors and duplicated controllers with a single configuration property and a version attribute, dramatically cutting API versioning code from hundreds of lines to just three.
Why API versioning is required
Legacy clients, mobile apps that cannot be forced to upgrade, third‑party integrations with independent release cycles, and fast‑evolving business requirements all make API versioning a practical necessity.
Old approaches in Spring Boot 3 and earlier
Duplicating a @RestController for each version, which caused code duplication, repeated bugs, scattered logic and no shared implementation.
Creating a custom HandlerInterceptor together with a custom @ApiVersion annotation, custom path matching, manual header/query parsing and manual deprecation‑header handling. This approach was highly invasive, hard to understand and became technical debt.
Spring Boot 4 core change: ApiVersionStrategy
Spring Boot 4 introduces a formal version abstraction called ApiVersionStrategy. It handles:
Version parsing (Header / Query / Path)
Version matching
Version range calculation
Deprecated / Sunset header generation
Semantic version parsing
Thus version control becomes a framework‑level capability.
New concise syntax
After upgrading, only one configuration line and a single version attribute are needed; the previous interceptor, path matcher and custom parsers are no longer required.
spring:
mvc:
apiversion:
use: header # or query-parameter, or path-segmentThree official version strategies
Header strategy (recommended for internal services)
# application.yml
spring:
mvc:
apiversion:
use: header
header: API-VersionRequest example:
curl -H "API-Version: 1.0" http://localhost:8080/api/ticketsPros: clean URL, suitable for service‑to‑service calls.
Cons: not visible in browsers, testing requires manual header addition.
Query‑parameter strategy (debug‑friendly)
# application.yml
spring:
mvc:
apiversion:
use: query-parameter
query-parameter: versionRequest example:
curl "http://localhost:8080/api/tickets?version=2.0"Pros: directly testable in browsers, high visibility.
Cons: URL becomes less tidy, may affect caching.
Path‑segment strategy (production‑grade)
# application.yml
spring:
mvc:
apiversion:
use: path-segment: 1Controller example:
package com.icoderoad.controller;
@RestController
@RequestMapping("/api/{version}/tickets")
public class TicketController {
@GetMapping(version = "1.0")
public TicketResponse getTicketsV1() { return new TicketResponse("V1 data"); }
@GetMapping(version = "2.0")
public TicketResponseV2 getTicketsV2() { return new TicketResponseV2("V2 data"); }
}Request example: curl http://localhost:8080/api/1.0/tickets Pros: explicit, CDN‑friendly, cache‑safe, easy routing.
Baseline version “+” support
The suffix + matches the specified version and any higher version, allowing unchanged interfaces to stay untouched.
@GetMapping(version = "1.0+")
public List<Ticket> getAllTickets() { return ticketService.findAll(); }In large systems this can reduce duplicate code by more than 60 %.
Built‑in deprecation mechanism
package com.icoderoad.config;
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
configurer.usePathSegment(1)
.addDeprecatedVersion(
"1.0",
LocalDate.of(2026, 6, 1),
"https://docs.icoderoad.com/migrate-v1-to-v2"
);
}
}When a client calls a deprecated version the response contains:
Deprecation: true
Sunset: Sat, 01 Jun 2026 00:00:00 GMT
Link: https://docs.icoderoad.com/migrate-v1-to-v2; rel="deprecation"Full production‑level example (electronic ticketing system)
package com.icoderoad.controller;
@RestController
@RequestMapping("/api/{version}/tickets")
public class TicketController {
private final TicketService ticketService;
public TicketController(TicketService ticketService) { this.ticketService = ticketService; }
// V1 basic list
@GetMapping(version = "1.0")
public ResponseEntity<List<TicketV1>> getTicketsV1() {
return ResponseEntity.ok(ticketService.findAll()
.stream()
.map(TicketV1::from)
.toList());
}
// V2 paginated list
@GetMapping(version = "2.0")
public ResponseEntity<TicketPageV2> getTicketsV2(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Page<Ticket> tickets = ticketService.findAll(PageRequest.of(page, size));
return ResponseEntity.ok(TicketPageV2.from(tickets));
}
// V3 filtered + real‑time status, using baseline "+"
@GetMapping(version = "3.0+")
public ResponseEntity<TicketPageV3> getTicketsV3(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String status,
@RequestParam(required = false) LocalDate date) {
Page<Ticket> tickets = ticketService.findWithFilters(page, size, status, date);
return ResponseEntity.ok(TicketPageV3.from(tickets));
}
}Test commands:
curl http://localhost:8080/api/1.0/tickets
curl "http://localhost:8080/api/2.0/tickets?page=0&size=10"
curl "http://localhost:8080/api/3.0/tickets?page=0&size=10&status=ACTIVE"Functional routing support
package com.icoderoad.config;
@Configuration
public class RouterConfig {
@Bean
public RouterFunction<ServerResponse> ticketRoutes() {
return RouterFunctions.route()
.GET("/api/tickets", version("1.0"), req -> ServerResponse.ok().body(ticketService.findAllV1()))
.GET("/api/tickets", version("2.0"), req -> ServerResponse.ok().body(ticketService.findAllV2()))
.build();
}
}Reality check: trade‑offs of each strategy
Header – elegant, but invisible to browsers.
Path – explicit and cache‑friendly.
Query – easy to test, but less tidy.
Spring Boot 4 does not force a choice; it reduces the implementation cost of any chosen strategy to zero.
Implementation cost drops to 0.
Conclusion
Version control has shifted from a custom engineering problem to a declarative framework capability. For new projects start with path‑segment versioning; existing custom versioning frameworks can be removed.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
LuTiao Programming
LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
