Skip to main content
Version: 1.9.x

Retry Mechanisms

The library provides sophisticated retry strategies to handle transient failures gracefully. You can configure a default retry policy for all handlers and optionally override it per handler.

Default Retry Policy​

The default retry policy applies to all handlers unless overridden. Configure it via application.yml or by providing a custom OutboxRetryPolicy bean.

Built-in Retry Policies​

Fixed Delay​

Retry with a constant delay between attempts:

namastack:
outbox:
retry:
policy: "fixed"
max-retries: 5
fixed:
delay: 5s # 5 seconds between retries

Use Case: Simple scenarios with consistent retry intervals

Example Retry Schedule: 0s → 5s → 5s → 5s → 5s → 5s → Failed

Linear Backoff​

Retry with linearly increasing delays:

namastack:
outbox:
retry:
policy: "linear"
max-retries: 5
linear:
initial-delay: 2s # Start with 2 seconds
increment: 2s # Add 2 seconds each retry
max-delay: 60s # Cap at 1 minute

Use Case: Gradually increasing delays for services that need time to recover

Example Retry Schedule: 0s → 2s → 4s → 6s → 8s → 10s → Failed

Exponential Backoff​

Retry with exponentially increasing delays:

namastack:
outbox:
retry:
policy: "exponential"
max-retries: 7
exponential:
initial-delay: 1s # Start with 1 second
max-delay: 60s # Cap at 1 minute
multiplier: 2.0 # Double each time

Use Case: Handles transient failures gracefully without overwhelming downstream services

Retry Schedule: 0s → 1s → 2s → 4s → 8s → 16s → 32s → 60s (capped)

Jittered Retry​

Add random jitter to prevent thundering herd problems. Jitter can be applied to any base policy (fixed, linear, or exponential):

namastack:
outbox:
retry:
policy: "exponential" # Can also be "fixed" or "linear"
max-retries: 7
exponential:
initial-delay: 2s
max-delay: 60s
multiplier: 2.0
jitter: 1s # Add [-1s, 1s] random delay

Benefits: Prevents coordinated retry storms when multiple instances retry simultaneously


Exception Filtering​

namastack:
outbox:
retry:
policy: exponential
max-retries: 3
exponential:
initial-delay: 1000
max-delay: 60000
multiplier: 2.0
# Only retry these exceptions
include-exceptions:
- java.net.SocketTimeoutException
- org.springframework.web.client.ResourceAccessException
- java.io.IOException
# Never retry these exceptions
exclude-exceptions:
- java.lang.IllegalArgumentException
- javax.validation.ValidationException
- com.example.BusinessException

Custom Default Retry Policy​

Implement the OutboxRetryPolicy interface and register it as a bean named outboxRetryPolicy:

@Configuration
class OutboxConfig {
@Bean("outboxRetryPolicy")
fun customRetryPolicy(): OutboxRetryPolicy {
return object : OutboxRetryPolicy {
override fun shouldRetry(exception: Throwable): Boolean {
// Don't retry validation errors
if (exception is IllegalArgumentException) return false
// Don't retry permanent failures
if (exception is PaymentDeclinedException) return false
// Retry transient failures
return exception is TimeoutException ||
exception is IOException ||
exception.cause is TimeoutException
}
override fun nextDelay(failureCount: Int): Duration {
// Exponential backoff: 1s → 2s → 4s → 8s (capped at 60s)
val delayMillis = 1000L * (1L shl failureCount)
return Duration.ofMillis(minOf(delayMillis, 60000L))
}
override fun maxRetries(): Int = 5
}
}
}

Key Methods:

  • shouldRetry(exception: Throwable): Boolean - Decide if this error should be retried
  • nextDelay(failureCount: Int): Duration - Calculate delay before next retry
  • maxRetries(): Int - Maximum number of retry attempts

Important: The bean must be named outboxRetryPolicy to override the default policy configured in application.yml.


Handler-Specific Retry Policies​

Override the default retry policy for specific handlers using @OutboxRetryable annotation or by implementing the OutboxRetryAware interface.

You can configure retry behavior per handler, allowing different handlers to have different retry strategies.

Interface-Based Approach​

Implement the OutboxRetryAware interface to specify a retry policy programmatically:

@Component
class PaymentHandler(
private val aggressiveRetryPolicy: AggressiveRetryPolicy
) : OutboxTypedHandler<PaymentEvent>, OutboxRetryAware {
override fun handle(payload: PaymentEvent, metadata: OutboxRecordMetadata) {
paymentGateway.process(payload)
}
override fun getRetryPolicy(): OutboxRetryPolicy = aggressiveRetryPolicy
}

@Component
class NotificationHandler(
private val conservativeRetryPolicy: ConservativeRetryPolicy
) : OutboxTypedHandler<NotificationEvent>, OutboxRetryAware {
override fun handle(payload: NotificationEvent, metadata: OutboxRecordMetadata) {
emailService.send(payload)
}
override fun getRetryPolicy(): OutboxRetryPolicy = conservativeRetryPolicy
}

@Component
class AggressiveRetryPolicy : OutboxRetryPolicy {
override fun shouldRetry(exception: Throwable) = true
override fun nextDelay(failureCount: Int) = Duration.ofMillis(500)
override fun maxRetries() = 10
}

@Component
class ConservativeRetryPolicy : OutboxRetryPolicy {
override fun shouldRetry(exception: Throwable) =
exception !is IllegalArgumentException
override fun nextDelay(failureCount: Int) = Duration.ofSeconds(10)
override fun maxRetries() = 2
}

Annotation-Based Approach​

Use the @OutboxRetryable annotation for method-level retry policy configuration:

@Component
class PaymentHandler {
// Critical handler - aggressive retries
@OutboxHandler
@OutboxRetryable(AggressiveRetryPolicy::class)
fun handlePayment(payload: PaymentEvent) {
paymentGateway.process(payload)
}

// Less critical handler - conservative retries
@OutboxHandler
@OutboxRetryable(ConservativeRetryPolicy::class)
fun handleNotification(payload: NotificationEvent) {
emailService.send(payload)
}

// Uses default retry policy
@OutboxHandler
fun handleAudit(payload: AuditEvent) {
auditService.log(payload)
}
}

Policy Resolution Order:

  1. Handler-specific policy via interface (OutboxRetryAware.getRetryPolicy()) - highest priority
  2. Handler-specific policy via annotation (@OutboxRetryable)
  3. Global custom policy (bean named outboxRetryPolicy)
  4. Default policy (from application.yml)

!!! tip "Interface vs Annotation"

  • Interface (OutboxRetryAware): Best when handler class is dedicated to single payload type, and you want type safety
  • Annotation (@OutboxRetryable): Best for method-level handlers or multiple handlers in one class

OutboxRetryPolicy.Builder API​

Use the fluent builder to compose robust retry policies without implementing the interface yourself.

@Configuration
class OutboxConfig {
@Bean
fun customRetryPolicy(): OutboxRetryPolicy {
return OutboxRetryPolicy.builder()
.maxRetries(5)
.exponentialBackoff(
initialDelay = Duration.ofSeconds(10),
multiplier = 2.0,
maxDelay = Duration.ofMinutes(5)
)
.jitter(Duration.ofSeconds(2))
.retryOn(TimeoutException::class.java, IOException::class.java)
.noRetryOn(IllegalArgumentException::class.java)
.build()
}
}

Builder at a glance:

  • Defaults: maxRetries = 3, fixedBackOff = 5s, jitter = 0, retry on all exceptions
  • Immutability: each method returns a new Builder; the original instance isn't mutated
  • Validation: durations must be > 0 (except jitter, which can be 0), multiplier > 1.0

Using the autoconfigured Builder​

A bean named outboxRetryPolicyBuilder is auto-configured from your namastack.outbox.retry.* application properties. Inject it to retain property-driven defaults and add programmatic customizations.

@Configuration
class OutboxConfig {
// Inject the autoconfigured builder from application.yml
@Bean
fun customRetryPolicy(
builder: OutboxRetryPolicy.Builder
): OutboxRetryPolicy {
// Start from property-based defaults, then refine
return builder
.retryOn(TimeoutException::class.java, IOException::class.java)
.noRetryOn(IllegalArgumentException::class.java)
.build()
}
}

Builder configuration options​

Backoff strategies:

  • Fixed: same delay for all retries
    fixedBackOff(Duration.ofSeconds(30))

  • Linear: incrementally increasing delay
    linearBackoff(initialDelay = Duration.ofSeconds(5), increment = Duration.ofSeconds(5), maxDelay = Duration.ofMinutes(2))

  • Exponential: exponentially increasing delay
    exponentialBackoff(initialDelay = Duration.ofSeconds(10), multiplier = 2.0, maxDelay = Duration.ofMinutes(5))

  • Custom: provide your own strategy
    backOff(myCustomBackOffStrategy)

Jitter:

Jitter randomizes the computed delay within [base - jitter, base + jitter] to avoid thundering herds; delays never go below zero.

OutboxRetryPolicy.builder()
.fixedBackOff(Duration.ofSeconds(30))
.jitter(Duration.ofSeconds(5)) // Actual delay: ~25-35 seconds

Exception rules and priority:

  • noRetryOn(): these exceptions are never retried (highest priority)
  • retryOn(): if specified, only these exceptions (or subclasses) are retried
  • retryIf(): predicate for advanced logic
  • Default: if neither retryOn() nor retryIf() is configured, all exceptions are retried; if any rule is configured but none match, do not retry
// Retry only on specific exceptions
OutboxRetryPolicy.builder()
.retryOn(TimeoutException::class.java, IOException::class.java)
.build()

// Retry all except specific exceptions
OutboxRetryPolicy.builder()
.noRetryOn(IllegalArgumentException::class.java, PaymentDeclinedException::class.java)
.build()

// Custom predicate for complex logic
OutboxRetryPolicy.builder()
.retryIf { exception ->
exception is RetryableException ||
(exception.cause is TimeoutException)
}
.build()

// Combine multiple rules (noRetryOn takes precedence)
OutboxRetryPolicy.builder()
.retryOn(IOException::class.java)
.noRetryOn(FileNotFoundException::class.java)
.retryIf { exception -> exception.message?.contains("transient") == true }
.build()

Complete examples​

// Simple policy with exponential backoff
val simplePolicy = OutboxRetryPolicy.builder()
.maxRetries(5)
.exponentialBackoff(
initialDelay = Duration.ofSeconds(10),
multiplier = 2.0,
maxDelay = Duration.ofMinutes(5)
)
.build()

// Advanced policy with all features
val advancedPolicy = OutboxRetryPolicy.builder()
.maxRetries(10)
.linearBackoff(
initialDelay = Duration.ofSeconds(5),
increment = Duration.ofSeconds(5),
maxDelay = Duration.ofMinutes(2)
)
.jitter(Duration.ofSeconds(2))
.retryOn(TimeoutException::class.java, IOException::class.java)
.noRetryOn(IllegalArgumentException::class.java)
.retryIf { exception ->
exception.message?.contains("retry", ignoreCase = true) == true
}
.build()