API Versioning
- Anand Nerurkar
- May 13
- 4 min read
how to handle api versioning
Handling API versioning is critical for SaaS platforms — especially multi-tenant architectures — to support backward compatibility, safe upgrades, and differentiated feature sets across tenants.
✅ API Versioning: Key Goals
Backward compatibility for existing clients
Safe evolution of APIs with new features
Support multiple versions in parallel for gradual rollout
Enable per-tenant feature control (optional)
🔧 Common API Versioning Strategies
1. URI Path Versioning (Most Common & RESTful)
bash
GET /api/v1/customers
GET /api/v2/customers
✅ Pros:
Simple and explicit
Easy to manage in routing/gateways
Developers see version clearly
❌ Cons:
Requires duplicating routes/controllers per version
2. Header-Based Versioning
http
GET /api/customers
Header: X-API-Version: v2
✅ Pros:
Cleaner URIs
Allows more dynamic routing/version control
❌ Cons:
Less visible to end users
Some tools (e.g., Swagger) need extra config
3. Query Parameter Versioning
bash
GET /api/customers?api-version=2
✅ Pros:
Easy to implement
Works well with API Gateway or custom logic
❌ Cons:
Feels less RESTful
Can cause caching issues
4. Media Type Versioning (Content Negotiation)
http
Accept: application/vnd.myapi.v1+json
✅ Pros:
Full control via headers
Aligns with hypermedia practices
❌ Cons:
Complex to implement and test
Hard to discover
🧱 Recommended Strategy for SaaS on Azure
Use URI versioning for major versionsCombine with feature flags or headers for minor feature control
🔁 Versioning Strategy in Microservices or Spring Boot + Azure
🔹 Codebase Structure
Use packages like com.insurance.api.v1.controller
Use Spring Profiles or API version annotations
Keep shared business logic in common libraries
🔹 API Gateway (Azure API Management)
Define multiple versions for the same API
Route /v1, /v2 endpoints to respective backend services
Apply version headers to control access if needed
🛠️ Azure Services to Support API Versioning
Component | Azure Service | Usage |
API Gateway | Azure API Management | Route versioned APIs, policies |
Feature Control | Azure App Configuration | Per-tenant feature toggles |
Authentication | Azure AD B2C | Token claims to carry version or tenant info |
Deployment Mgmt | Azure DevOps | Canary deploys for new versions |
Documentation | Swagger / OpenAPI | Version-specific docs (/v1/api-docs) |
📋 Best Practices
Never break existing versions → Keep old versions until clients migrate.
Deprecate with notice → Mark versions as deprecated, notify clients.
Use semver: Major (breaking), minor (new), patch (bugfix)
Monitor usage → Track which versions tenants are using.
Automate with CI/CD → Build, test, and deploy each version.
📦 Sample URI Strategy
API Call | Description |
GET /api/v1/claims | Old claims format |
GET /api/v2/claims | New model, more fields |
POST /api/v1/claims/submit | Legacy submission flow |
POST /api/v2/claims/submit | New flow + fraud check |
Spring Boot implementation guide
Here’s a Spring Boot implementation guide for API versioning, designed specifically for a multi-tenant SaaS architecture with major version control and optional per-tenant feature flags.
✅ Goals of This Guide
Implement clean API versioning using Spring Boot
Support multiple API versions (v1, v2, etc.)
Allow per-tenant control if needed
Ensure maintainable structure for long-term evolution
🏗️ 1. Folder Structure (Per Version)
css
src/main/java
│ ├── controller
│ │ ├── v1
│ │ │ └── ClaimControllerV1.java
│ │ └── v2
│ │ └── ClaimControllerV2.java
│ ├── service
│ │ ├── ClaimService.java (shared)
│ └── model
│ ├── v1
│ │ └── ClaimRequestV1.java
│ └── v2
│ └── ClaimRequestV2.java
📘 2. Controller Implementation
java
@RestController
@RequestMapping("/api/v1/claims")
public class ClaimControllerV1 {
@Autowired
private ClaimService claimService;
@GetMapping("/{id}")
public ResponseEntity<ClaimResponseV1> getClaim(@PathVariable Long id) {
return ResponseEntity.ok(claimService.getClaimV1(id));
}
}
java
@RestController
@RequestMapping("/api/v2/claims")
public class ClaimControllerV2 {
@Autowired
private ClaimService claimService;
@GetMapping("/{id}")
public ResponseEntity<ClaimResponseV2> getClaim(@PathVariable Long id) {
return ResponseEntity.ok(claimService.getClaimV2(id));
}
}
🧠 3. Shared Service Layer
java
@Service
public class ClaimService {
public ClaimResponseV1 getClaimV1(Long id) {
// Map internal model to V1 format
}
public ClaimResponseV2 getClaimV2(Long id) {
// Map internal model to V2 format (with more fields, logic)
}
}
🛡️ 4. Optional: Header-Based Versioning (Advanced)
Instead of URL versioning, use X-API-Version header:
🔸 Custom @ApiVersion Annotation
java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
String value();
}
🔸 Version-Aware Request Mapping Handler
Use Spring’s RequestMappingHandlerMapping to route based on custom header:
java
@Configuration
public class ApiVersioningConfig extends WebMvcConfigurationSupport {
@Override
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
return new ApiVersionRequestMappingHandlerMapping();
}
}
💡 For simplicity, URL versioning is preferred unless you require dynamic versioning via headers.
🧪 5. Swagger Integration for Multiple Versions
Add Swagger Config per Version:
java
@Bean
public Docket apiV1() {
return new Docket(DocumentationType.OAS_30)
.groupName("v1")
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.controller.v1"))
.paths(PathSelectors.any())
.build()
.apiInfo(apiInfo("1.0"));
}
@Bean
public Docket apiV2() {
return new Docket(DocumentationType.OAS_30)
.groupName("v2")
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.controller.v2"))
.paths(PathSelectors.any())
.build()
.apiInfo(apiInfo("2.0"));
}
🧩 6. Azure API Management Mapping (Optional)
In Azure APIM:
Create multiple API versions: /api/v1/*, /api/v2/*
Route them to Spring Boot backend via appropriate path mapping or header
You can also restrict older versions per tenant using policies
📊 7. Per-Tenant Version Control (Optional)
Use a Feature Management System (e.g., Azure App Configuration, LaunchDarkly):
java
if (tenantConfig.get("api-version").equals("v2")) {
return claimService.getClaimV2(id);
} else {
return claimService.getClaimV1(id);
}
You can also use:
TenantContext extracted from JWT token or header
Spring AOP for conditional routing based on tenant features
🧼 Best Practices
Practice | Why it Matters |
Use clear versioned URLs | Easier documentation and testing |
Keep backward compatibility | Avoid breaking existing tenants |
Document deprecated versions | Help tenants plan migration |
Automate tests for each API version | Ensure consistent behavior |
Apply tenant-specific feature toggles | Gradual rollouts, A/B testing support |
Comments