API versioning is critical for mobile applications where users don’t always update to the latest version. While API contract testing ensures compatibility between frontend and backend, versioning strategy handles the complexity of supporting multiple client versions in production. This guide covers versioning strategies, backward compatibility, forced updates, and testing multiple API versions simultaneously.
Versioning Strategies
URL Versioning
// Clear, explicit versioning in URL path
val BASE_URL_V1 = "https://api.example.com/v1/"
val BASE_URL_V2 = "https://api.example.com/v2/"
interface ApiServiceV1 {
@GET("users/{id}")
suspend fun getUser(@Path("id") id: String): UserV1
}
interface ApiServiceV2 {
@GET("users/{id}")
suspend fun getUser(@Path("id") id: String): UserV2
}
Header Versioning
class VersionInterceptor(private val apiVersion: String) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.addHeader("API-Version", apiVersion)
.build()
return chain.proceed(request)
}
}
// Usage
val client = OkHttpClient.Builder()
.addInterceptor(VersionInterceptor("2.0"))
.build()
Content Negotiation
@Headers("Accept: application/vnd.example.v2+json")
@GET("users/{id}")
suspend fun getUserV2(@Path("id") id: String): UserV2
Backward Compatibility Testing
Multi-Version Test Suite
Testing multiple API versions requires comprehensive coverage across all supported client versions. As discussed in API testing mastery, a well-structured test suite validates both functional and non-functional requirements.
class MultiVersionApiTest {
@Test
fun testUserEndpointV1() = runTest {
val apiV1 = createApiClient(version = "v1")
val user = apiV1.getUser("123")
// V1 response structure
assertNotNull(user.id)
assertNotNull(user.name)
assertNotNull(user.email)
}
@Test
fun testUserEndpointV2() = runTest {
val apiV2 = createApiClient(version = "v2")
val user = apiV2.getUser("123")
// V2 adds additional fields
assertNotNull(user.id)
assertNotNull(user.fullName) // renamed from 'name'
assertNotNull(user.email)
assertNotNull(user.phoneNumber) // new field
assertNotNull(user.preferences) // new nested object
}
@Test
fun testDeprecationWarnings() = runTest {
val apiV1 = createApiClient(version = "v1")
val response = apiV1.getUserRaw("123")
// Check for deprecation header
val deprecationWarning = response.headers()["Deprecation-Warning"]
assertTrue(deprecationWarning?.contains("This API version is deprecated") == true)
}
}
Compatibility Layer
// Adapter pattern for version compatibility
interface UserRepository {
suspend fun getUser(id: String): User
}
class UserRepositoryV1(private val api: ApiServiceV1) : UserRepository {
override suspend fun getUser(id: String): User {
val userV1 = api.getUser(id)
return User(
id = userV1.id,
fullName = userV1.name, // map old 'name' to 'fullName'
email = userV1.email,
phoneNumber = null, // not available in V1
preferences = UserPreferences() // default for V1
)
}
}
class UserRepositoryV2(private val api: ApiServiceV2) : UserRepository {
override suspend fun getUser(id: String): User {
return api.getUser(id) // direct mapping
}
}
// Factory
class RepositoryFactory {
fun createUserRepository(apiVersion: String): UserRepository {
return when (apiVersion) {
"v1" -> UserRepositoryV1(apiV1)
"v2" -> UserRepositoryV2(apiV2)
else -> throw UnsupportedVersionException()
}
}
}
@Test
fun testCompatibilityLayer() = runTest {
val repoV1 = RepositoryFactory().createUserRepository("v1")
val repoV2 = RepositoryFactory().createUserRepository("v2")
val userFromV1 = repoV1.getUser("123")
val userFromV2 = repoV2.getUser("123")
// Both return same User model
assertEquals(userFromV1.id, userFromV2.id)
assertEquals(userFromV1.fullName, userFromV2.fullName)
}
Force Update Strategy
Version Check Endpoint
data class VersionResponse(
val minimumVersion: String,
val latestVersion: String,
val forceUpdate: Boolean,
val message: String
)
interface VersionCheckService {
@GET("version/check")
suspend fun checkVersion(
@Query("platform") platform: String,
@Query("currentVersion") currentVersion: String
): VersionResponse
}
class VersionChecker(private val versionService: VersionCheckService) {
suspend fun checkForUpdate(): UpdateStatus {
val currentVersion = BuildConfig.VERSION_NAME
val platform = "android"
val response = versionService.checkVersion(platform, currentVersion)
return when {
response.forceUpdate -> UpdateStatus.ForceUpdate(response.message)
isNewerVersion(currentVersion, response.latestVersion) ->
UpdateStatus.OptionalUpdate(response.latestVersion)
else -> UpdateStatus.UpToDate
}
}
private fun isNewerVersion(current: String, latest: String): Boolean {
val currentParts = current.split(".").map { it.toInt() }
val latestParts = latest.split(".").map { it.toInt() }
for (i in 0 until maxOf(currentParts.size, latestParts.size)) {
val currentPart = currentParts.getOrElse(i) { 0 }
val latestPart = latestParts.getOrElse(i) { 0 }
if (latestPart > currentPart) return true
if (latestPart < currentPart) return false
}
return false
}
}
@Test
fun testVersionComparison() {
val checker = VersionChecker(mockService)
assertTrue(checker.isNewerVersion("1.0.0", "1.0.1"))
assertTrue(checker.isNewerVersion("1.0.0", "2.0.0"))
assertFalse(checker.isNewerVersion("1.0.1", "1.0.0"))
assertFalse(checker.isNewerVersion("1.0.0", "1.0.0"))
}
@Test
fun testForceUpdateDetection() = runTest {
mockService.respondWith(VersionResponse(
minimumVersion = "2.0.0",
latestVersion = "2.1.0",
forceUpdate = true,
message = "Please update to continue"
))
val status = VersionChecker(mockService).checkForUpdate()
assertTrue(status is UpdateStatus.ForceUpdate)
assertEquals("Please update to continue", (status as UpdateStatus.ForceUpdate).message)
}
Force Update UI
class ForceUpdateActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val message = intent.getStringExtra("message") ?: "Update required"
AlertDialog.Builder(this)
.setTitle("Update Required")
.setMessage(message)
.setCancelable(false)
.setPositiveButton("Update") { _, _ ->
openPlayStore()
}
.show()
}
private fun openPlayStore() {
val appPackageName = packageName
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$appPackageName")))
} catch (e: ActivityNotFoundException) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$appPackageName")))
}
}
}
Graceful Degradation
Feature Flags Based on API Version
class FeatureManager(private val apiVersion: String) {
fun isFeatureAvailable(feature: Feature): Boolean {
return when (feature) {
Feature.USER_PREFERENCES -> apiVersion >= "2.0"
Feature.PUSH_NOTIFICATIONS -> apiVersion >= "1.5"
Feature.DARK_MODE -> apiVersion >= "2.1"
else -> true
}
}
}
@Composable
fun UserProfile(featureManager: FeatureManager) {
Column {
Text("User Profile")
if (featureManager.isFeatureAvailable(Feature.USER_PREFERENCES)) {
PreferencesSection()
}
if (featureManager.isFeatureAvailable(Feature.DARK_MODE)) {
ThemeToggle()
}
}
}
@Test
fun testFeatureAvailability() {
val managerV1 = FeatureManager("1.0")
val managerV2 = FeatureManager("2.0")
assertFalse(managerV1.isFeatureAvailable(Feature.USER_PREFERENCES))
assertTrue(managerV2.isFeatureAvailable(Feature.USER_PREFERENCES))
}
A/B Testing Different API Versions
A/B testing API versions in mobile applications allows gradual rollout and performance comparison before full deployment.
Gradual Rollout Strategy
class ApiVersionSelector(
private val userId: String,
private val rolloutPercentage: Int
) {
fun selectApiVersion(): String {
val userHash = userId.hashCode().absoluteValue
val bucket = userHash % 100
return if (bucket < rolloutPercentage) {
"v2" // New version
} else {
"v1" // Old version
}
}
}
@Test
fun testGradualRollout() {
// 20% rollout
val selector = ApiVersionSelector("user123", rolloutPercentage = 20)
val versions = (1..1000).map {
ApiVersionSelector("user$it", 20).selectApiVersion()
}
val v2Count = versions.count { it == "v2" }
val percentage = (v2Count.toDouble() / versions.size) * 100
// Should be approximately 20%
assertTrue(percentage in 15.0..25.0)
}
Analytics for Version Performance
In microservices architectures, tracking metrics across API versions becomes even more critical for identifying performance bottlenecks and compatibility issues.
class ApiMetrics {
fun trackApiCall(version: String, endpoint: String, success: Boolean, latency: Long) {
analytics.logEvent("api_call") {
param("version", version)
param("endpoint", endpoint)
param("success", success)
param("latency_ms", latency)
}
}
}
class VersionAwareInterceptor(private val metrics: ApiMetrics) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val version = extractVersion(request)
val startTime = System.currentTimeMillis()
val response = chain.proceed(request)
val latency = System.currentTimeMillis() - startTime
val success = response.isSuccessful
metrics.trackApiCall(version, request.url.encodedPath, success, latency)
return response
}
}
Deprecation Strategy
Gradual Deprecation Process
data class DeprecationInfo(
val deprecatedAt: String,
val sunsetDate: String,
val migrationGuide: String
)
@GET("users/{id}")
suspend fun getUser(
@Path("id") id: String,
@Header("API-Version") version: String = "1.0"
): Response<User>
// Server returns deprecation headers:
// Deprecation-Warning: API v1.0 is deprecated. Please migrate to v2.0
// Sunset: 2024-12-31
// Migration-Guide: https://docs.example.com/migration/v1-to-v2
Client-Side Deprecation Handling
class DeprecationMonitor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
val deprecationWarning = response.header("Deprecation-Warning")
val sunsetDate = response.header("Sunset")
if (deprecationWarning != null) {
log.warn("API Deprecation: $deprecationWarning (Sunset: $sunsetDate)")
analytics.logEvent("api_deprecation_warning") {
param("warning", deprecationWarning)
param("sunset_date", sunsetDate ?: "unknown")
}
}
return response
}
}
Best Practices
- Version in URL for major changes - Clear, cache-friendly
- Use headers for minor changes - Flexible, backward compatible
- Always support N-1 versions - Give users time to update
- Implement force update mechanism - Critical security/bug fixes
- Use feature flags - Gradual rollout, A/B testing
- Monitor version distribution - Know when to sunset old versions
- Communicate deprecation clearly - Advance notice, migration guides
Conclusion
API versioning for mobile requires:
- Multi-version testing strategy
- Backward compatibility layers
- Force update mechanisms
- Graceful degradation
- A/B testing infrastructure
- Clear deprecation timelines
Proper versioning ensures smooth transitions while maintaining support for existing users, balancing innovation with stability.