The @Retry annotation allows you to specify a custom retry policy for services, workflows, or individual methods using a WithRetry class.
Package
io.infinitic.annotations.Retry
Targets
- Classes (services and workflows)
- Methods (service methods and workflow methods)
Parameters
with
KClass<out WithRetry>
required
The class implementing the retry policy (must implement WithRetry interface)
WithRetry Interface
To define a retry policy, implement the WithRetry interface:
import io.infinitic.tasks.WithRetry
import io.infinitic.exceptions.tasks.TaskFailedException
class MyRetryPolicy : WithRetry {
override fun getSecondsBeforeRetry(
retry: Int,
exception: TaskFailedException
): Double? {
// Return number of seconds to wait, or null to stop retrying
return when {
retry >= 5 -> null // Stop after 5 retries
else -> (retry * 2.0) // Exponential backoff: 0, 2, 4, 6, 8 seconds
}
}
}
Usage
On Classes
Apply a retry policy to all methods in a service or workflow:
import io.infinitic.annotations.Retry
@Retry(with = ExponentialRetryPolicy::class)
interface PaymentService {
fun processPayment(amount: Double): PaymentResult
fun refundPayment(transactionId: String): RefundResult
}
@Retry(with = WorkflowRetryPolicy::class)
interface BookingWorkflow {
fun processBooking(bookingId: String): BookingResult
}
On Methods
Apply different retry policies to specific methods:
import io.infinitic.annotations.Retry
interface PaymentService {
@Retry(with = AggressiveRetryPolicy::class)
fun chargeCard(cardInfo: CardInfo): PaymentResult
@Retry(with = ConservativeRetryPolicy::class)
fun refund(transactionId: String): RefundResult
}
Retry Policy Examples
Exponential Backoff
import io.infinitic.tasks.WithRetry
import io.infinitic.exceptions.tasks.TaskFailedException
class ExponentialRetryPolicy : WithRetry {
override fun getSecondsBeforeRetry(
retry: Int,
exception: TaskFailedException
): Double? {
return when {
retry >= 10 -> null // Max 10 retries
else -> Math.pow(2.0, retry.toDouble()) // 1, 2, 4, 8, 16... seconds
}
}
}
Linear Backoff
class LinearRetryPolicy : WithRetry {
override fun getSecondsBeforeRetry(
retry: Int,
exception: TaskFailedException
): Double? {
return when {
retry >= 5 -> null
else -> (retry + 1) * 5.0 // 5, 10, 15, 20, 25 seconds
}
}
}
Conditional Retry
import java.net.SocketTimeoutException
import java.sql.SQLException
class ConditionalRetryPolicy : WithRetry {
override fun getSecondsBeforeRetry(
retry: Int,
exception: TaskFailedException
): Double? {
// Get the root cause
val cause = exception.cause?.cause
return when {
// Don't retry validation errors
cause is IllegalArgumentException -> null
// Retry network errors aggressively
cause is SocketTimeoutException -> {
if (retry >= 20) null else 1.0
}
// Retry database errors with backoff
cause is SQLException -> {
if (retry >= 5) null else (retry * 3.0)
}
// Default: retry up to 3 times with 10 second delay
else -> if (retry >= 3) null else 10.0
}
}
}
Fixed Delay
class FixedDelayRetryPolicy : WithRetry {
override fun getSecondsBeforeRetry(
retry: Int,
exception: TaskFailedException
): Double? {
return when {
retry >= 3 -> null
else -> 30.0 // Always wait 30 seconds between retries
}
}
}
class ImmediateRetryPolicy : WithRetry {
override fun getSecondsBeforeRetry(
retry: Int,
exception: TaskFailedException
): Double? {
return when {
retry >= 3 -> null
else -> 0.0 // Retry immediately
}
}
}
Method vs Class Priority
When both class and method have @Retry annotations, the method annotation takes precedence:
@Retry(with = DefaultRetryPolicy::class)
interface PaymentService {
// Uses DefaultRetryPolicy
fun processPayment(amount: Double): PaymentResult
// Uses SpecialRetryPolicy (overrides class-level)
@Retry(with = SpecialRetryPolicy::class)
fun processCriticalPayment(amount: Double): PaymentResult
}
Integration with Configuration
Retry policies can also be configured in YAML (takes precedence over annotations):
services:
- name: PaymentService
class: com.example.PaymentServiceImpl
executor:
concurrency: 10
withRetry: com.example.CustomRetryPolicy
Best Practices
- Use exponential backoff for external services: Prevents overwhelming downstream systems
- Set reasonable maximum retries: Avoid infinite retry loops
- Consider the error type: Not all errors should be retried (e.g., validation errors)
- Log retry attempts: Include retry count in logs for debugging
- Use different policies for different criticality: Critical operations may need more retries
- Account for total duration: Consider maximum time spent retrying
Complete Example
import io.infinitic.annotations.Retry
import io.infinitic.tasks.WithRetry
import io.infinitic.exceptions.tasks.TaskFailedException
import java.net.ConnectException
// Define retry policy
class PaymentRetryPolicy : WithRetry {
override fun getSecondsBeforeRetry(
retry: Int,
exception: TaskFailedException
): Double? {
val cause = exception.cause?.cause
return when {
// Don't retry business logic errors
cause is IllegalArgumentException -> null
cause is IllegalStateException -> null
// Retry network errors up to 10 times with exponential backoff
cause is ConnectException -> {
if (retry >= 10) null else Math.pow(2.0, retry.toDouble())
}
// Default: retry 5 times with linear backoff
else -> if (retry >= 5) null else (retry * 2.0)
}
}
}
// Apply to service
@Retry(with = PaymentRetryPolicy::class)
interface PaymentService {
fun processPayment(amount: Double): PaymentResult
// This method never retries
@Retry(with = NoRetryPolicy::class)
fun validatePayment(amount: Double): ValidationResult
}
class NoRetryPolicy : WithRetry {
override fun getSecondsBeforeRetry(
retry: Int,
exception: TaskFailedException
): Double? = null // Never retry
}
// Implementation
class PaymentServiceImpl : PaymentService {
override fun processPayment(amount: Double): PaymentResult {
// This will be retried according to PaymentRetryPolicy
return callExternalPaymentGateway(amount)
}
override fun validatePayment(amount: Double): ValidationResult {
// This will never be retried
require(amount > 0) { "Amount must be positive" }
return ValidationResult.valid()
}
}
Monitoring Retries
Retries generate events that can be monitored:
class PaymentServiceImpl : PaymentService {
override fun processPayment(amount: Double): PaymentResult {
val attempt = Task.retry // Current retry attempt (0 for first try)
if (attempt > 0) {
logger.warn("Retrying payment processing, attempt: $attempt")
}
return callPaymentGateway(amount)
}
}
See Also