Capped exponential backoff factories validate the initial interval on first use instead of at construction

Author: jjh75607Created Sep 10, 2026Updated Sep 10, 2026

Resilience4j version: 2.4.0 and current master (a8a3316)

Java version: 21

Problem

The capped factories validate the maximum interval eagerly but build their uncapped delegate inside the returned lambda, so the initial interval is only validated on first use.

java
IntervalFunction.ofExponentialBackoff(0, 2.0);          // throws IllegalArgumentException
IntervalFunction.ofExponentialBackoff(0, 2.0, 5000);    // returns normally

ofExponentialRandomBackoff(long, double, double, long) is the same.

Zero is legal elsewhere, since RetryConfig.Builder.waitDuration takes Duration.ZERO as retry immediately, but it cannot mean that here: ofExponentialBackoff(initial, multiplier) is of(initial, x -> x * multiplier), so a zero initial interval stays zero forever.

Configuration reaches it. wait-duration: 0 passes CommonRetryConfigurationProperties.setWaitDuration, which rejects only negatives. With enable-exponential-backoff and a multiplier set, exponential-max-wait-duration decides which factory is used: without it the two-argument overload rejects the value while the registry is built, with it the capped overload accepts it.

RetryImpl calls the interval function outside the try block that rethrows the exception being retried, so the validation error replaces the original failure. Today this fails with IllegalArgumentException: Illegal argument interval: 0 millis is less than 1 rather than the exception it was retrying:

java
@Test
void exceptionBeingRetriedShouldSurviveTheRetry() {
    RetryConfig config = RetryConfig.custom()
        .maxAttempts(3)
        .intervalFunction(IntervalFunction.ofExponentialBackoff(0, 2.0, 5000))
        .build();
    Retry retry = Retry.of("example", config);
    Supplier<String> supplier = Retry.decorateSupplier(retry, () -> {
        throw new IllegalStateException("real failure");
    });

    assertThatThrownBy(supplier::get)
        .isInstanceOf(IllegalStateException.class)
        .hasMessage("real failure");
}

Compatibility

Checking eagerly is a behaviour change: an application with that configuration starts today, and would fail to start instead. #2108 made the same move for the randomization factor in one of these two factories.

Source: resilience4j/resilience4j