# Versioning Strategies

These are the following ways in which we can do the API versioning -

* URI versioning
* Header versioning
* Content negotiation
* Version migration strategies

### 1. URI Path Versioning

This is the most straightforward approach, where the URL path includes the version details.&#x20;

**Example**

```plaintext
GET /v1/users
GET /v2/users
```

**Pros**

* **Clear and Explicit:** The version is immediately visible to developers in the URI.
* **Cache-Friendly:** Works well with caching systems since the version is part of the URL.
* **Backward Compatibility:** Allows clients to continue using older versions while new versions are introduced.

**Cons**

* **Clutters URLs:** Adds redundancy to the URI.
* **Limited Flexibility:** Changing versions requires updating the client’s integration.

```java
@RestController
@RequestMapping("/api/v1")
public class OrderControllerV1 {
    
    @GetMapping("/orders/{orderId}")
    public ResponseEntity<OrderResponseV1> getOrder(@PathVariable String orderId) {
        // V1 implementation
        return ResponseEntity.ok(orderService.getOrderV1(orderId)); // v1 service
    }
}

@RestController
@RequestMapping("/api/v2")
public class OrderControllerV2 {
    
    @GetMapping("/orders/{orderId}")
    public ResponseEntity<OrderResponseV2> getOrder(@PathVariable String orderId) {
        // V2 implementation with enhanced response
        return ResponseEntity.ok(orderService.getOrderV2(orderId)); // v2 service 
    }
}
```

### 2. Header Versioning

This approach uses custom headers to specify the API version.

**Pros**

* **Clean URIs:** Keeps the URL clean and focused on resource identification.
* **Flexible:** Allows finer-grained control, such as specifying minor versions (`1.1`, `1.2`).

**Cons**

* **Less Discoverable:** The version information is not visible in the URL.
* **Cache Complexity:** Requires more sophisticated caching mechanisms since caching is based on headers.

**Best Practices**

* Use the `Accept` header with media type versioning.\
  **Example:** `Accept: application/vnd.api+json; version=2.0`
* Alternatively, use a custom header.\
  **Example:** `X-API-Version: 2`

```java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    @GetMapping("/{orderId}")
    public ResponseEntity<?> getOrder(
            @PathVariable String orderId,
            @RequestHeader(value = "X-API-Version", defaultValue = "1") int version) {
        
        switch (version) {
            case 1:
                OrderResponseV1 responseV1 = orderService.getOrderV1(orderId);
                return ResponseEntity.ok(responseV1);
                
            case 2:
                OrderResponseV2 responseV2 = orderService.getOrderV2(orderId);
                return ResponseEntity.ok(responseV2);
                
            default:
                throw new UnsupportedVersionException("API version " + version + " is not supported");
        }
    }
}

// Version handler configuration
@Configuration
public class VersioningConfig {
    
    @Bean
    public WebMvcConfigurer versioningConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(new ApiVersionInterceptor());
            }
        };
    }
}
```

Api Version Interceptor

```java
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;

public class ApiVersionInterceptor implements HandlerInterceptor {
    
    private static final Logger logger = LoggerFactory.getLogger(ApiVersionInterceptor.class);
    private static final String VERSION_HEADER = "X-API-Version";
    private static final int MIN_VERSION = 1;
    private static final int MAX_VERSION = 2;

    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        
        String versionHeader = request.getHeader(VERSION_HEADER);
        
        // If no version header is provided, default to version 1
        if (versionHeader == null || versionHeader.isEmpty()) {
            request.setAttribute("api-version", 1);
            logger.debug("No API version specified. Defaulting to version 1");
            return true;
        }

        try {
            int version = Integer.parseInt(versionHeader);
            
            // Validate version range
            if (version < MIN_VERSION || version > MAX_VERSION) {
                logger.warn("Unsupported API version requested: {}", version);
                response.setStatus(HttpStatus.BAD_REQUEST.value());
                response.getWriter().write(
                    String.format("API version %d is not supported. Supported versions are %d through %d",
                                version, MIN_VERSION, MAX_VERSION)
                );
                return false;
            }

            // Store the version in request attributes for later use
            request.setAttribute("api-version", version);
            logger.debug("API Version {} validated successfully", version);
            return true;

        } catch (NumberFormatException e) {
            logger.error("Invalid API version format: {}", versionHeader);
            response.setStatus(HttpStatus.BAD_REQUEST.value());
            response.getWriter().write("Invalid API version format. Version must be a number");
            return false;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, 
                         HttpServletResponse response, 
                         Object handler, 
                         ModelAndView modelAndView) throws Exception {
        // Add version header to response for clarity
        int version = (int) request.getAttribute("api-version");
        response.setHeader(VERSION_HEADER, String.valueOf(version));
    }

    @Override
    public void afterCompletion(HttpServletRequest request, 
                              HttpServletResponse response, 
                              Object handler, 
                              Exception ex) throws Exception {
        // Cleanup if needed
        request.removeAttribute("api-version");
    }
}
```

### 3. Accept Header Versioning (Content Negotiation)

This strategy uses content negotiation through the Accept header. It determines the API version based on the `Content-Type` or `Accept` headers.

**Example**

```http
GET /users
Accept: application/vnd.company.users+json; version=1
```

**Pros**

* **Advanced Flexibility:** Supports versioning, content formats, and feature negotiation simultaneously.
* **Clean URIs:** Does not add version information to the URL.

**Cons**

* **Complex Implementation:** More challenging to implement and configure on the server side.
* **Discoverability:** Version information is less obvious compared to URI versioning.

**Best Practices**

* Use MIME types for content negotiation.\
  **Example:** `application/vnd.company.resource-v1+json`
* Clearly document available content types and their associated versions.

```java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    @GetMapping(
        value = "/{orderId}",
        produces = "application/vnd.company.app-v1+json"
    )
    public ResponseEntity<OrderResponseV1> getOrderV1(@PathVariable String orderId) {
        return ResponseEntity.ok(orderService.getOrderV1(orderId));
    }
    
    @GetMapping(
        value = "/{orderId}",
        produces = "application/vnd.company.app-v2+json"
    )
    public ResponseEntity<OrderResponseV2> getOrderV2(@PathVariable String orderId) {
        return ResponseEntity.ok(orderService.getOrderV2(orderId));
    }
}
```

### 4. Query Parameter Versioning

While not as common, some APIs use query parameters for versioning.

```java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    @GetMapping("/{orderId}")
    public ResponseEntity<?> getOrder(
            @PathVariable String orderId,
            @RequestParam(defaultValue = "1") int version) {
        
        return switch (version) {
            case 1 -> ResponseEntity.ok(orderService.getOrderV1(orderId));
            case 2 -> ResponseEntity.ok(orderService.getOrderV2(orderId));
            default -> throw new UnsupportedVersionException("Version not supported");
        };
    }
}
```

### Advanced Versioning Strategies

#### Feature Toggles with Versioning

```java
@Service
public class OrderService {
    
    private final FeatureToggleService featureToggleService;
    
    public OrderResponse getOrder(String orderId, int version) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new ResourceNotFoundException("Order not found"));
            
        OrderResponse.OrderResponseBuilder builder = OrderResponse.builder()
            .orderId(order.getId())
            .status(order.getStatus());
            
        // Add features based on version and toggles
        if (version >= 2 && featureToggleService.isEnabled("ENHANCED_ORDER_DETAILS")) {
            builder.enhancedDetails(getEnhancedDetails(order));
        }
        
        if (version >= 2 && featureToggleService.isEnabled("ORDER_ANALYTICS")) {
            builder.analytics(getOrderAnalytics(order));
        }
        
        return builder.build();
    }
}
```

#### Graceful Deprecation

```java
@RestController
@RequestMapping("/api/v1")
public class OrderControllerV1 {
    
    @GetMapping("/orders/{orderId}")
    @Deprecated
    @Header("Deprecation: date=2024-12-31")
    @Header("Link: </api/v2/orders/{orderId}>; rel=\"successor-version\"")
    public ResponseEntity<OrderResponseV1> getOrder(@PathVariable String orderId) {
        // Log deprecation metrics
        deprecationMetricsService.recordDeprecatedEndpointUsage(
            "GET /api/v1/orders/{orderId}");
            
        return ResponseEntity.ok(orderService.getOrderV1(orderId));
    }
}
```

### Best Practices for API Versioning

1. **Version Strategy Selection** Consider these factors when choosing a strategy:

   * Client requirements
   * Infrastructure constraints
   * Developer experience
   * Maintenance overhead

2. **Backwards Compatibility**

   ```java
   public class OrderResponseV2 implements OrderResponse {
       // New fields
       private List<OrderItemV2> items;
       private EnhancedShippingDetails shipping;
       
       // Backward compatibility method
       public OrderResponseV1 toV1Format() {
           return OrderResponseV1.builder()
               .orderId(this.orderId)
               .status(this.status)
               .items(this.items.stream()
                   .map(OrderItemV2::toV1Format)
                   .collect(Collectors.toList()))
               .build();
       }
   }
   ```

3. **Documentation**

   ```java
   @OpenAPIDefinition(
       info = @Info(
           title = "E-commerce API",
           version = "2.0",
           description = "API documentation with version support"
       )
   )
   @RestController
   public class OrderController {
       
       @Operation(summary = "Get order details", 
                 description = "Version 2 includes enhanced order details")
       @ApiResponse(
           responseCode = "200",
           description = "Order retrieved successfully",
           content = @Content(schema = @Schema(implementation = OrderResponseV2.class))
       )
       @GetMapping(
           value = "/api/v2/orders/{orderId}",
           produces = "application/json"
       )
       public ResponseEntity<OrderResponseV2> getOrderV2(...) {
           // Implementation
       }
   }
   ```

#### **Comparison of Strategies**

<table data-header-hidden data-full-width="true"><thead><tr><th></th><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody><tr><td><strong>Strategy</strong></td><td><strong>Visibility</strong></td><td><strong>Ease of Use</strong></td><td><strong>Flexibility</strong></td><td><strong>Cache Support</strong></td><td><strong>Common Use Case</strong></td></tr><tr><td>URI Versioning</td><td>High (in URL)</td><td>Easy</td><td>Low</td><td>Excellent</td><td>Public APIs with simple versioning needs.</td></tr><tr><td>Header Versioning</td><td>Low (in headers)</td><td>Moderate</td><td>High</td><td>Complex</td><td>APIs requiring clean URLs and flexibility.</td></tr><tr><td>Content Negotiation</td><td>Low (in headers)</td><td>Complex</td><td>Very High</td><td>Moderate</td><td>Advanced APIs with multiple content types.</td></tr></tbody></table>
