Spring REST API Versioning: The Complete Guide (2024 Edition)
Master REST API versioning in Spring Boot 3.2+ with production-proven strategies.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Java 17+
- ✓Spring Boot 3.2+
- ✓Maven 3.9+ or Gradle 8.x
- ✓Basic understanding of REST principles
Use URI path versioning for public APIs (e.g., /v1/orders) and Accept header versioning for internal microservices. Never use request parameter versioning in production. Spring Boot 3.2+ supports both via @RequestMapping and custom WebMvcConfigurer.
Imagine you run a pizza restaurant. You change the recipe, but some customers still want the old recipe. API versioning is like keeping both the old and new recipe available, so no one gets angry. In Spring, you do this by having different endpoints (like /v1/pizza and /v2/pizza) or by letting clients tell you which version they want via a special header.
Let me be blunt: API versioning is one of the most misunderstood topics in REST architecture. I've seen teams spend weeks debating it, only to ship something that breaks production on day one. This isn't academic โ it's about keeping your payment system running while you roll out new features. Spring Boot 3.2+ gives you several ways to handle this, but most developers pick the wrong one. Here's the hard truth: most teams get this wrong because they treat versioning as a technical problem when it's really a contract negotiation with your clients.
1. Why Versioning Matters (And Why You'll Regret Skipping It)
Stop pretending your API won't change. It will. The moment you have a single client in production, you've made an implicit contract. Breaking that contract is not just bad practice โ it's a breach of trust that can cost you customers and money. In Spring Boot, versioning isn't hard, but doing it right requires understanding the trade-offs. I've seen this blow up in production when teams used request parameter versioning like ?version=1. It works until it doesn't โ caching proxies and CDNs treat those as different URLs, but your API gateway might not. The result? Stale data served to clients who think they're getting the latest version. Always version at the infrastructure level, not the application level.
@RequestMapping with explicit version paths. Avoid request parameters โ they break caching and are harder to document.2. What the Official Docs Won't Tell You
The Spring Boot documentation is great for getting started, but it won't tell you that header versioning can be a nightmare in production. Let me be blunt: if you're using Accept header versioning with custom media types like application/vnd.myapp.v1+json, you're setting yourself up for pain. The issue? Most API clients don't set Accept headers correctly. I've debugged countless cases where browsers or mobile apps send / as the Accept header, which causes Spring to fall back to the first matching handler โ often the wrong version. The official docs recommend WebMvcConfigurer for custom header parsing, but they don't warn you about the HttpMediaTypeNotAcceptableException that will haunt your logs. Instead, use a custom HandlerInterceptor to explicitly parse version headers and route accordingly. This gives you full control and proper error handling.
3. URI Path Versioning: The Simplest (and Most Reliable) Approach
URI path versioning is the gold standard for public APIs. It's explicit, cache-friendly, and easy to debug. When you see /v1/orders and /v2/orders, there's no ambiguity. In Spring Boot 3.2, you implement this by creating separate controller classes for each version. Stop doing the "one controller with if-else" pattern. That's a trap that will burn you. I've seen a codebase where a single OrderController had 15 methods, each with if (version == 1) ... else if (version == 2) .... It was unmaintainable. Instead, use separate packages: com.example.orders.v1 and com.example.orders.v2. Each has its own @RestController with @RequestMapping("/api/v1/orders") and @RequestMapping("/api/v2/orders"). This keeps your code clean and your deployments safe. You can even deploy different versions to different servers if needed.
4. Accept Header Versioning: When to Use It (and When to Run Away)
Accept header versioning is elegant for internal microservices where you control both sides. You send Accept: application/vnd.myapp.v2+json and get the right version. But here's the hard truth: most teams get this wrong because they don't handle the / wildcard. I've seen this blow up in production when a new microservice started sending Accept: / and Spring matched the wrong controller. The fix? Implement a custom WebMvcConfigurer that parses the Accept header and throws a clear error if the version is ambiguous. Spring Boot 3.2+ has improved this with ContentNegotiationConfigurer, but you still need to be explicit. Never rely on default behavior. If you're doing Accept header versioning without a custom HandlerInterceptor, you're doing it wrong. The risk is too high for production systems handling payments or user data.
X-Version-Routed to responses so clients can verify which version they received. This is invaluable for debugging.5. Request Parameter Versioning: Why You Should Never Use It
Let me be blunt: request parameter versioning (?version=1) is a trap that will burn you. It seems simple, but it breaks every caching layer in existence. CDNs like Cloudflare and Akamai treat ?version=1 and ?version=2 as different cache keys, which is fine. But the problem is that many API gateways and load balancers strip query parameters, causing version information to be lost. I've seen this blow up in production when a team used ?version=1 and their NGINX reverse proxy was configured to ignore query parameters for caching. Clients got the wrong version randomly. The fix was to move to URI path versioning, but by then, the damage was done โ 2 days of data corruption. Spring Boot 3.2+ supports request parameter versioning via @RequestParam, but just because you can doesn't mean you should. If you're using request parameter versioning in a public API, you're doing it wrong. Period.
?version=2 to /v2/resource before hitting your controllers.6. Custom Versioning Strategy: Building a Version Router
Sometimes the standard approaches don't fit. Maybe you need to version based on a custom header, or route based on client IP range. Spring Boot 3.2+ allows you to build a custom version router using HandlerInterceptor and WebMvcConfigurer. This is advanced, but it's the only way to handle complex scenarios like A/B testing or gradual rollouts. I've built this for a real-time analytics platform where different data centers needed different API versions. The key is to implement a HandlerInterceptor that reads the version from a header, cookie, or even a JWT claim, then sets a request attribute. Your controllers can then check this attribute. But be careful: custom versioning is a double-edged sword. It's flexible, but it's also hard to document and debug. Only use this when URI path or header versioning doesn't work. Otherwise, you're over-engineering.
X-Version-Routed header to every response so clients can verify the version they received. This single header has saved me hours of debugging.7. Versioning with Spring Cloud Gateway
If you're using Spring Cloud Gateway (3.1.x+), you can handle versioning at the gateway level before requests reach your microservices. This is powerful because it centralizes version routing and allows you to migrate traffic gradually. For example, you can route 10% of traffic to /v2/orders and 90% to /v1/orders using weight-based routing. Spring Cloud Gateway supports this via the Weight route predicate. But here's the hard truth: most teams get this wrong because they forget to add version information to the downstream request. If you strip the version at the gateway, your microservices won't know which version to serve. Always add a header like X-Version-Routed before forwarding. I've seen this blow up in production when a team routed traffic to the wrong microservice because the gateway didn't preserve the version context.
8. Testing and Deprecating API Versions
Versioning without testing is like deploying without a parachute. You need integration tests that explicitly test each version. In Spring Boot 3.2, use @WebMvcTest with separate test classes for each version controller. But more importantly, test the version routing itself. I've seen teams write perfect controller tests but forget to test that the version header actually routes to the correct controller. For deprecation, always add a Sunset header and a Deprecation header to old versions. Spring Boot doesn't do this automatically โ you need a custom filter. I've seen this blow up in production when a team deprecated a version but forgot to add the headers. Clients had no warning, and when the endpoint was removed, they broke. Stop ignoring deprecation headers. They're not optional. If you're deprecating an API without a sunset policy and client notification, you're doing it wrong.
The Payment Gateway Outage of 2023
java.lang.NoSuchMethodError: 'void com.example.payment.v1.PaymentController.processPayment(java.lang.String)'/v1/payments endpoint because analytics showed zero traffic for 30 days./v1/payments and didn't follow HTTP redirects. When we removed the endpoint, their requests failed silently for 4 hours before anyone noticed./v1/payments to /v2/payments with a deprecation warning header, and implemented a sunset date policy.- Never assume zero usage means zero traffic.
- Always redirect old versions with clear deprecation headers.
- Monitor for at least 90 days before removing an endpoint.
| File | Command / Code | Purpose |
|---|---|---|
| PaymentControllerV1.java | @RestController | 1. Why Versioning Matters (And Why You'll Regret Skipping It |
| VersionInterceptor.java | public class VersionInterceptor implements HandlerInterceptor { | 2. What the Official Docs Won't Tell You |
| OrderControllerV2.java | @RestController | 3. URI Path Versioning |
| WebConfig.java | @Configuration | 4. Accept Header Versioning |
| BadExampleController.java | @RestController | 5. Request Parameter Versioning |
| CustomVersionRouter.java | @Component | 6. Custom Versioning Strategy |
| application.yml | spring: | 7. Versioning with Spring Cloud Gateway |
| DeprecationFilter.java | @Component | 8. Testing and Deprecating API Versions |
Key takeaways
, Sunset: <date>`) to old versions. Monitor traffic before removing them.Interview Questions on This Topic
How would you implement API versioning in a Spring Boot microservices architecture?
com.example.orders.v1, com.example.orders.v2). I'd add a custom filter to include Deprecation and Sunset headers for old versions. For gradual rollouts, I'd use weight-based routing at the gateway.Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Boot. Mark it forged?
4 min read · try the examples if you haven't