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

  1. Version in URL for major changes - Clear, cache-friendly
  2. Use headers for minor changes - Flexible, backward compatible
  3. Always support N-1 versions - Give users time to update
  4. Implement force update mechanism - Critical security/bug fixes
  5. Use feature flags - Gradual rollout, A/B testing
  6. Monitor version distribution - Know when to sunset old versions
  7. 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.