Skip to main content

Command Palette

Search for a command to run...

Design Patterns: Retry /w Kotlin

Dealing with transient failures or intermittent issues using Retry Design Pattern in Kotlin.

Updated
4 min read
Design Patterns: Retry /w Kotlin
R

Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.

Introduction

In software development, dealing with transient failures or intermittent issues is a common challenge. One effective way to address this challenge is by implementing retry mechanisms. Retry design patterns enable developers to automatically retry an operation that has failed, with the hope that subsequent attempts will succeed. In this article, we will explore various retry design patterns in Kotlin, their implementation, and when to use them.

1. Why Retry?:

  • Transient Failures: Some failures are temporary and may be resolved with subsequent attempts.

  • Network Issues: Connection problems or timeouts might be resolved by retrying.

  • External Dependencies: When relying on external services, occasional failures are inevitable.

2. Common Retry Strategies:

  • Fixed Retry: Retry a fixed number of times with a constant delay.

  • Exponential Backoff: Increase the delay exponentially with each retry.

  • Randomized Backoff: Introduce randomness in the retry delay to avoid synchronized retries.

3. Implementing Retry:

  • Using Higher-Order Functions in Kotlin:

fun <T> retry(
    times: Int = 3,
    initialDelay: Long = 1000,
    maxDelay: Long = 10000,
    factor: Double = 2.0,
    block: () -> T
): T? {
    var currentDelay = initialDelay
    repeat(times - 1) {
        try {
            return block()
        } catch (e: Exception) {
            // Log or handle the exception
        }
        Thread.sleep(currentDelay)
        currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
    }
    return block() // One last attempt without delay
}
  • Using Retry Libraries - Kotlin Exponential Backoff:
implementation("io.github.benas.random-beans:random-beans:4.0.0")
val result: ResultType? = ExponentialBackoff()
    .retryOn(Exception::class.java)
    .maxRetries(3)
    .intervalFunction { attempt -> 1000 * 2.0.pow(attempt.toDouble()).toLong() }
    .call { yourFunction() }

4. Examples:

  • Simple Retry:

fun fetchData(): String {
    return retry {
        // Code to fetch data
        throw RuntimeException("Network error")
    } ?: throw RuntimeException("Failed after multiple attempts")
}
  • Using Exponential Backoff:

fun fetchDataWithExponentialBackoff(): String {
    return ExponentialBackoff<String>()
        .retryOn(Exception::class.java)
        .maxRetries(3)
        .intervalFunction { attempt -> 1000 * 2.0.pow(attempt.toDouble()).toLong() }
        .call {
            // Code to fetch data
            throw RuntimeException("Network error")
        } ?: throw RuntimeException("Failed after multiple attempts")
}

5. Custom RetryHelper with Example :

Here is a simple example /w RetryHelper uses a function that operates with retry logic.

import kotlinx.coroutines.delay
import kotlin.random.Random

class RetryHelper(
    private val maxRetries: Int = 3,
    private val initialDelayMillis: Long = 100,
    private val maxDelayMillis: Long = 1000
) {
    suspend fun <T> retry(
        operation: suspend () -> T
    ): T {
        var currentRetry = 0
        var delayMillis = initialDelayMillis

        while (true) {
            try {
                return operation()
            } catch (e: Exception) {
                if (currentRetry >= maxRetries) {
                    throw e
                }

                // Exponential backoff with jitter
                val backoffTime = delayMillis + Random.nextLong(-delayMillis / 2, delayMillis / 2)
                delay(backoffTime)

                currentRetry++
                delayMillis = minOf(delayMillis * 2, maxDelayMillis)
            }
        }
    }
}

suspend fun performNetworkRequest(): String {
    // Simulating a network request that might fail
    if (Random.nextBoolean()) {
        throw RuntimeException("Network request failed")
    }

    return "Successful response"
}

// Example of using the RetryHelper
suspend fun main() {
    val retryHelper = RetryHelper()

    val result = retryHelper.retry {
        // Your operation that might fail
        performNetworkRequest()
    }

    println("Result: $result")
}

In this example, the RetryHelper class provides a retry function that takes a suspend function operation. The retry function attempts to execute the operation and catches any exceptions. If an exception occurs, it checks if the maximum number of retries (maxRetries) has been reached. If not, it applies an exponential backoff with jitter and retries the operation after a delay.

The main function demonstrates how to use the RetryHelper to perform a network request using the performNetworkRequest function. We can customize the parameters of the RetryHelper (e.g., maxRetries, initialDelayMillis, maxDelayMillis) based on our specific requirements.

This example uses Kotlin coroutines for asynchronous programming, and the delay function is used to introduce a delay between retry attempts. Adjust the parameters and error-handling logic according to our application's needs.


Conclusion

Retry design patterns are essential for creating robust and resilient systems, especially when dealing with external dependencies and network operations. Kotlin, with its concise syntax and powerful features, allows developers to easily implement and customize retry strategies. Whether using simple higher-order functions or dedicated libraries, understanding and applying retry patterns can significantly improve the reliability of our applications. Consider the nature of our application and the specific requirements when choosing a retry strategy.


That's it for today. Happy Coding...

B

If you dont immediately prompt the user that a network error has happened, you are sacrificing the perception of the app for the network. You are delaying a conclusion for the user. That the network is not working. Someone from the help desk will call you some day and will tell you he has users that are looking at progresbars or empty screens. And you will do some digging and you will see the app is retrying 3 times and there is a 12 second timeout on each request. User will not even stay on that screen for that long! It will then hit you how appropriate is after one timed out request to promt the user with an informative message and have him click retry if he wants to. The perception that some request fails just once rarely is false. Some people may work in a building that has a poor router. Or go to a bar that has a poor or crowded router and hang around. 3 out of 5 requests may consistently time out in that place indefinitely. IMHO Inform the user fast a request has failed and dont test his patience. This may get them upset! He wont be upset to manually retry with one tap.

R

I agree with your point. But this article is focused on Retry Design Pattern with some basic implementation and the implementation depends on dev's use case.

Design Patterns

Part 1 of 1