Skip to main content

Retry

← all patterns

pattern

Executes an operation, retrying on failure up to a configured number of attempts — with exponential backoff, optional jitter, and an optional overall deadline across the whole loop.

01

Execution flow

  • Attempt — the operation runs.
  • Success — returns immediately; a Success event is emitted and no further attempts are made.
  • Failure — an AttemptFailed event is emitted, then shouldRetry is tested against the thrown exception.
  • Not retryableshouldRetry returns false → a Rejected event is emitted and RetryRejectedException is thrown, even if this was the last available attempt.
  • Retryable, but out of budget — the attempt count has reached maxAttempts, or the overall deadline has already elapsed → an Exhausted event is emitted and RetryExhaustedException is thrown.
  • Retryable, budget remains — the loop sleeps for the current backoff delay (with jitter applied and maxDelayMs clamping it), then grows the delay by backoffMultiplier and tries again.
  • Interrupted during the sleep — the thread's interrupt status is restored, an Interrupted event is emitted, and RetryInterruptedException is thrown carrying the failure that was about to be retried.
  • Deadline check after the sleep — if the overall deadline passed while sleeping, the loop stops as Exhausted without making another attempt.

02

Behavior

maxAttempts counts the first call as an attempt — withMaxAttempts(1) disables retrying entirely. Backoff grows exponentially: the first retry waits initialDelayMs, and every subsequent delay is the previous one multiplied by backoffMultiplier. maxDelayMs caps every delay, including the initial one and after jitter is applied — a delay above the cap is clamped, not rejected.

Jitter shifts each delay by a uniformly random offset within [delay * (1 - jitterFactor), delay * (1 + jitterFactor)], spreading out retries from clients that failed at the same moment (thundering herd). A jitterFactor of 0.0 (the default) disables jitter.

An optional overall deadline (withOverallDeadline) bounds the total wall-clock time spent across all attempts and backoff waits, measured from the first attempt. It is checked only between attempts — it never preempts an attempt already in progress, which stays a per-attempt Timeout's responsibility. Once it elapses, the loop stops exactly as if the attempt budget itself had run out.

shouldRetry is evaluated before the attempt count and the deadline are checked. This means a non-retryable exception always produces RetryRejectedException, even on the last attempt, rather than being reported as RetryExhaustedException. By default, shouldRetry retries only IOException and its subclasses — other exceptions are treated as permanent failures.

03

Configuration

propertyrequireddescription
nameyesIdentifier used in events and exceptions (instance-specific)
withMaxAttempts()noTotal attempts, including the first call. Default: 3
withInitialDelay()noWait before the first retry, in milliseconds. Default: 100ms
withBackoffMultiplier()noFactor each delay is multiplied by after a failed attempt. Default: 2.0
withMaxDelay()noCap on every backoff delay, applied after jitter. Default: uncapped
withJitter()noUniform randomization factor for each delay, in [0.0, 1.0]. Default: 0.0 (off)
withOverallDeadline()noTotal wall-clock budget across all attempts and backoff waits, checked between attempts. Default: uncapped
withShouldRetry()noPredicate deciding whether a thrown exception is retried. Default: retries only IOException and subclasses
withListener()noSubscribe to retry events (e.g. Success, AttemptFailed, Exhausted)
withClock()noUse a custom Clock instead of system (mainly for testing)

04

Events & failure

  • Success — the operation succeeded. Carries: timestamp, name, total attempts taken.
  • AttemptFailed — an attempt was made and failed. Carries: timestamp, name, attempt number, the thrown exception.
  • RejectedshouldRetry declined to retry a failure before the attempt budget was exhausted. Carries: timestamp, name, attempt number, the rejected exception.
  • Exhausted — all attempts failed, whether because maxAttempts was reached or the overall deadline elapsed. Carries: timestamp, name, total attempts, the last exception.
  • Interrupted — the thread was interrupted while waiting for a backoff delay. Carries: timestamp, name, attempt number, the last real failure (not the interrupt itself).

Throws RetryExhaustedException when the attempt budget or overall deadline runs out — produced from the Exhausted path. Throws RetryRejectedException when shouldRetry declines to retry — produced from the Rejected path. Throws RetryInterruptedException when the thread is interrupted during a backoff wait — produced from the Interrupted path. All three carry attemptCount (total attempts made, including the first) and the relevant failure via getCause(); only call() throws them — outcome() instead returns a Failure outcome wrapping the same cause.