**Approach (brief)**Design a RetryFramework that:- Distinguishes idempotent vs non-idempotent calls- For non-idempotent: generates/sends idempotency key header on first attempt and persists it so retries survive restarts- Supports configurable backoff, maxAttempts, and key cleanup policy**Kotlin pseudocode**kotlin
// Interfaces
interface Request { val url: String; val headers: MutableMap<String,String>; val body: ByteArray? }
interface HttpClient { suspend fun send(req: Request): HttpResponse }
data class RetryConfig(val maxAttempts: Int = 5, val baseDelayMs: Long = 200, val maxDelayMs: Long = 5000,
val jitter: Boolean = true)
// Simple persistence (file/DB abstraction)
interface KeyStore { fun save(keyId: String, key: String); fun load(keyId: String): String?; fun delete(keyId: String) }
// Retry framework
class RetryFramework(private val client: HttpClient, private val keyStore: KeyStore, private val config: RetryConfig) {
suspend fun sendWithRetry(req: Request, idempotent: Boolean, keyId: String? = null): HttpResponse {
var attempt = 0
val idempKey = if (!idempotent) {
// reuse existing persisted key if present, or generate+persist
keyId?.let { keyStore.load(it) } ?: run {
val generated = UUID.randomUUID().toString()
keyId?.let { keyStore.save(it, generated) }
generated
}
} else null
while (true) {
attempt++
// attach idempotency header only for non-idempotent
idempKey?.let { req.headers["Idempotency-Key"] = it }
val resp = try { client.send(req) } catch (e: Throwable) { null }
if (resp != null && resp.isSuccessful()) {
// success -> cleanup persisted key when server confirms idempotent outcome
if (idempKey != null && keyId != null) keyStore.delete(keyId)
return resp
}
if (attempt >= config.maxAttempts) {
// give up; keep key persisted so external retry after restart can continue (policy)
throw RetryFailedException("failed after $attempt")
}
// backoff with optional jitter
val delayMs = calculateBackoff(config.baseDelayMs, attempt, config.maxDelayMs, config.jitter)
delay(delayMs)
}
}
private fun calculateBackoff(base: Long, attempt: Int, max: Long, jitter: Boolean): Long {
val exp = min(max.toDouble(), base * 2.0.pow(attempt - 1)).toLong()
return if (jitter) (exp / 2 + Random.nextLong(exp / 2)) else exp
}
}
**Key points & trade-offs**- Persist idempotency key keyed by a developer-provided keyId (e.g., orderId) so restarts can continue retries.- Cleanup only after confirmed success to avoid accidental duplicate side-effects.- Keep maxAttempts local but allow long-running/retry-on-restart via persisted key and external orchestration.- Consider secure storage (KeyStore) and TTL-based cleanup (GC job to remove old keys).