Introduction to Retry Pattern
The Retry Pattern is a resilience pattern that enables applications to handle transient failures by transparently retrying operations. It's particularly useful for network calls, database operations, and external service integrations where temporary failures are common.
Why Use Retry Pattern?
- Handle Transient Failures: Network timeouts, temporary unavailability
- Improve Resilience: Automatic recovery from intermittent issues
- Reduce Manual Intervention: Automatic retry logic
- Configurable Behavior: Customizable retry strategies and conditions
Core Components
1. Retry Strategies
2. Backoff Policies
3. Retry Conditions
4. Circuit Breaker Integration
Basic Retry Implementation
Simple Retry Utility
package com.example.retry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
public class SimpleRetry {
private static final Logger logger = LoggerFactory.getLogger(SimpleRetry.class);
private final int maxAttempts;
private final long delayMs;
private final Predicate<Exception> retryCondition;
public SimpleRetry(int maxAttempts, long delayMs) {
this(maxAttempts, delayMs, ex -> true);
}
public SimpleRetry(int maxAttempts, long delayMs, Predicate<Exception> retryCondition) {
this.maxAttempts = maxAttempts;
this.delayMs = delayMs;
this.retryCondition = retryCondition;
}
public <T> T execute(Callable<T> operation) throws Exception {
Exception lastException = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
logger.debug("Attempt {}/{}", attempt, maxAttempts);
return operation.call();
} catch (Exception e) {
lastException = e;
if (!retryCondition.test(e) || attempt == maxAttempts) {
logger.warn("Non-retryable exception or max attempts reached", e);
throw e;
}
logger.warn("Attempt {}/{} failed: {}. Retrying in {}ms",
attempt, maxAttempts, e.getMessage(), delayMs);
if (delayMs > 0) {
Thread.sleep(delayMs);
}
}
}
throw lastException;
}
public void execute(Runnable operation) throws Exception {
execute(() -> {
operation.run();
return null;
});
}
// Builder method for fluent configuration
public static Builder builder() {
return new Builder();
}
public static class Builder {
private int maxAttempts = 3;
private long delayMs = 1000;
private Predicate<Exception> retryCondition = ex -> true;
public Builder maxAttempts(int maxAttempts) {
this.maxAttempts = maxAttempts;
return this;
}
public Builder delayMs(long delayMs) {
this.delayMs = delayMs;
return this;
}
public Builder retryOn(Predicate<Exception> retryCondition) {
this.retryCondition = retryCondition;
return this;
}
public Builder retryOn(Class<? extends Exception> exceptionType) {
this.retryCondition = ex -> exceptionType.isInstance(ex);
return this;
}
public SimpleRetry build() {
return new SimpleRetry(maxAttempts, delayMs, retryCondition);
}
}
}
Usage Examples
package com.example.retry;
import java.io.IOException;
import java.net.ConnectException;
import java.util.concurrent.Callable;
public class SimpleRetryExamples {
public static void main(String[] args) {
SimpleRetry retry = SimpleRetry.builder()
.maxAttempts(3)
.delayMs(1000)
.retryOn(IOException.class)
.build();
// Example 1: Basic retry
try {
String result = retry.execute(() -> {
// Simulate unreliable operation
if (Math.random() > 0.3) {
throw new IOException("Temporary connection failure");
}
return "Success";
});
System.out.println("Result: " + result);
} catch (Exception e) {
System.err.println("All attempts failed: " + e.getMessage());
}
// Example 2: Retry with specific exception
SimpleRetry networkRetry = SimpleRetry.builder()
.maxAttempts(5)
.delayMs(2000)
.retryOn(ex -> ex instanceof ConnectException ||
ex.getMessage().contains("timeout"))
.build();
// Example 3: Runnable operation
try {
retry.execute(() -> {
System.out.println("Executing operation...");
if (Math.random() > 0.5) {
throw new RuntimeException("Temporary failure");
}
});
} catch (Exception e) {
System.err.println("Operation failed: " + e.getMessage());
}
}
}
Advanced Retry Implementation
Configurable Retry Template
package com.example.retry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Predicate;
public class RetryTemplate<T> {
private static final Logger logger = LoggerFactory.getLogger(RetryTemplate.class);
private final int maxAttempts;
private final Duration initialDelay;
private final double multiplier;
private final Duration maxDelay;
private final Set<Class<? extends Throwable>> retryableExceptions;
private final Set<Class<? extends Throwable>> nonRetryableExceptions;
private final Predicate<Throwable> customRetryCondition;
private final Consumer<RetryContext> onRetry;
private final Consumer<RetryContext> onFailure;
private final Consumer<RetryContext> onSuccess;
private RetryTemplate(Builder<T> builder) {
this.maxAttempts = builder.maxAttempts;
this.initialDelay = builder.initialDelay;
this.multiplier = builder.multiplier;
this.maxDelay = builder.maxDelay;
this.retryableExceptions = builder.retryableExceptions;
this.nonRetryableExceptions = builder.nonRetryableExceptions;
this.customRetryCondition = builder.customRetryCondition;
this.onRetry = builder.onRetry;
this.onFailure = builder.onFailure;
this.onSuccess = builder.onSuccess;
}
public T execute(Callable<T> operation) throws Exception {
RetryContext context = new RetryContext();
Throwable lastException = null;
for (context.attempt = 1; context.attempt <= maxAttempts; context.attempt++) {
try {
logger.debug("Execution attempt {}/{}", context.attempt, maxAttempts);
T result = operation.call();
context.setResult(result);
if (onSuccess != null) {
onSuccess.accept(context);
}
logger.info("Operation completed successfully on attempt {}", context.attempt);
return result;
} catch (Exception e) {
lastException = e;
context.setLastException(e);
if (!shouldRetry(e) || context.attempt == maxAttempts) {
logger.warn("Non-retryable exception or max attempts reached", e);
context.setExhausted(true);
if (onFailure != null) {
onFailure.accept(context);
}
throw e;
}
logger.warn("Attempt {}/{} failed: {}",
context.attempt, maxAttempts, e.getMessage());
if (onRetry != null) {
onRetry.accept(context);
}
long delay = calculateDelay(context.attempt);
logger.info("Retrying in {}ms (attempt {})", delay, context.attempt + 1);
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Retry interrupted", ie);
}
}
}
throw new RuntimeException("All retry attempts exhausted", lastException);
}
private boolean shouldRetry(Throwable e) {
// Check non-retryable exceptions first
for (Class<? extends Throwable> nonRetryable : nonRetryableExceptions) {
if (nonRetryable.isInstance(e)) {
return false;
}
}
// Check retryable exceptions
for (Class<? extends Throwable> retryable : retryableExceptions) {
if (retryable.isInstance(e)) {
return true;
}
}
// Check custom condition
return customRetryCondition != null && customRetryCondition.test(e);
}
private long calculateDelay(int attempt) {
if (attempt == 1) {
return initialDelay.toMillis();
}
long delay = (long) (initialDelay.toMillis() * Math.pow(multiplier, attempt - 1));
return Math.min(delay, maxDelay.toMillis());
}
public static <T> Builder<T> builder() {
return new Builder<>();
}
public static class Builder<T> {
private int maxAttempts = 3;
private Duration initialDelay = Duration.ofSeconds(1);
private double multiplier = 2.0;
private Duration maxDelay = Duration.ofSeconds(30);
private Set<Class<? extends Throwable>> retryableExceptions = new HashSet<>();
private Set<Class<? extends Throwable>> nonRetryableExceptions = new HashSet<>();
private Predicate<Throwable> customRetryCondition;
private Consumer<RetryContext> onRetry;
private Consumer<RetryContext> onFailure;
private Consumer<RetryContext> onSuccess;
public Builder<T> maxAttempts(int maxAttempts) {
this.maxAttempts = maxAttempts;
return this;
}
public Builder<T> initialDelay(Duration initialDelay) {
this.initialDelay = initialDelay;
return this;
}
public Builder<T> exponentialBackoff(double multiplier, Duration maxDelay) {
this.multiplier = multiplier;
this.maxDelay = maxDelay;
return this;
}
public Builder<T> retryOn(Class<? extends Throwable>... exceptions) {
this.retryableExceptions.addAll(Arrays.asList(exceptions));
return this;
}
public Builder<T> notRetryOn(Class<? extends Throwable>... exceptions) {
this.nonRetryableExceptions.addAll(Arrays.asList(exceptions));
return this;
}
public Builder<T> retryWhen(Predicate<Throwable> condition) {
this.customRetryCondition = condition;
return this;
}
public Builder<T> onRetry(Consumer<RetryContext> onRetry) {
this.onRetry = onRetry;
return this;
}
public Builder<T> onFailure(Consumer<RetryContext> onFailure) {
this.onFailure = onFailure;
return this;
}
public Builder<T> onSuccess(Consumer<RetryContext> onSuccess) {
this.onSuccess = onSuccess;
return this;
}
public RetryTemplate<T> build() {
return new RetryTemplate<>(this);
}
}
public static class RetryContext {
private int attempt;
private Throwable lastException;
private Object result;
private boolean exhausted;
public int getAttempt() { return attempt; }
public Throwable getLastException() { return lastException; }
public Object getResult() { return result; }
public boolean isExhausted() { return exhausted; }
void setLastException(Throwable lastException) { this.lastException = lastException; }
void setResult(Object result) { this.result = result; }
void setExhausted(boolean exhausted) { this.exhausted = exhausted; }
}
}
Usage Examples
package com.example.retry;
import java.io.IOException;
import java.net.ConnectException;
import java.time.Duration;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException;
public class RetryTemplateExamples {
public static void main(String[] args) {
// Example 1: Basic exponential backoff
RetryTemplate<String> retry1 = RetryTemplate.<String>builder()
.maxAttempts(5)
.initialDelay(Duration.ofSeconds(1))
.exponentialBackoff(2.0, Duration.ofSeconds(30))
.retryOn(IOException.class, TimeoutException.class)
.notRetryOn(IllegalArgumentException.class)
.onRetry(context -> {
System.out.printf("Retry attempt %d after %s%n",
context.getAttempt(), context.getLastException().getMessage());
})
.onSuccess(context -> {
System.out.printf("Success after %d attempts%n", context.getAttempt());
})
.onFailure(context -> {
System.err.printf("Failed after %d attempts%n", context.getAttempt());
})
.build();
try {
String result = retry1.execute(() -> {
// Simulate unreliable service
double random = Math.random();
if (random < 0.7) {
throw new IOException("Service temporarily unavailable");
}
return "Service response";
});
System.out.println("Final result: " + result);
} catch (Exception e) {
System.err.println("Operation failed: " + e.getMessage());
}
// Example 2: Custom retry condition
RetryTemplate<Integer> retry2 = RetryTemplate.<Integer>builder()
.maxAttempts(3)
.initialDelay(Duration.ofMillis(500))
.retryWhen(ex -> ex.getMessage() != null &&
ex.getMessage().contains("retry"))
.build();
// Example 3: Network operation with specific exceptions
RetryTemplate<String> networkRetry = RetryTemplate.<String>builder()
.maxAttempts(4)
.initialDelay(Duration.ofSeconds(2))
.exponentialBackoff(1.5, Duration.ofSeconds(10))
.retryOn(ConnectException.class, TimeoutException.class)
.notRetryOn(SecurityException.class)
.onRetry(ctx -> {
System.out.printf("Network retry %d for %s%n",
ctx.getAttempt(), ctx.getLastException().getClass().getSimpleName());
})
.build();
}
}
Spring-based Retry Implementation
Spring Retry Configuration
package com.example.retry.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.context.annotation.Bean;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableRetry
public class RetryConfig {
@Bean
public RetryTemplate simpleRetryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
// Fixed backoff - 2 seconds between retries
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(2000L); // 2 seconds
// Retry policy - max 3 attempts
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(3);
retryTemplate.setBackOffPolicy(backOffPolicy);
retryTemplate.setRetryPolicy(retryPolicy);
return retryTemplate;
}
@Bean
public RetryTemplate exponentialRetryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
// Exponential backoff
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(1000L); // 1 second
backOffPolicy.setMultiplier(2.0); // Double the interval each retry
backOffPolicy.setMaxInterval(30000L); // Max 30 seconds
// Retry policy with specific exceptions
Map<Class<? extends Throwable>, Boolean> retryableExceptions = new HashMap<>();
retryableExceptions.put(IllegalStateException.class, true);
retryableExceptions.put(TimeoutException.class, true);
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(5, retryableExceptions);
retryTemplate.setBackOffPolicy(backOffPolicy);
retryTemplate.setRetryPolicy(retryPolicy);
return retryTemplate;
}
@Bean
public RetryTemplate circuitBreakerRetryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
// Circuit breaker-like behavior
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(2); // Fewer attempts for circuit breaker
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(5000L); // Longer delay
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.setBackOffPolicy(backOffPolicy);
return retryTemplate;
}
}
Spring Retry Service
package com.example.retry.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.net.ConnectException;
import java.time.LocalDateTime;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class NetworkService {
private static final Logger logger = LoggerFactory.getLogger(NetworkService.class);
private final AtomicInteger callCount = new AtomicInteger(0);
private final RetryTemplate retryTemplate;
public NetworkService(RetryTemplate exponentialRetryTemplate) {
this.retryTemplate = exponentialRetryTemplate;
}
/**
* Method-level retry with Spring @Retryable
*/
@Retryable(
value = {IOException.class, TimeoutException.class},
maxAttempts = 4,
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 10000)
)
public String callExternalService(String serviceUrl) throws IOException, TimeoutException {
int attempt = callCount.incrementAndGet();
logger.info("Calling external service {} (attempt {})", serviceUrl, attempt);
// Simulate different failure scenarios
double random = Math.random();
if (random < 0.6) {
throw new IOException("Service temporarily unavailable");
} else if (random < 0.8) {
throw new TimeoutException("Request timeout");
}
return "Response from " + serviceUrl + " at " + LocalDateTime.now();
}
/**
* Recovery method when all retries exhausted
*/
@Recover
public String recover(IOException e, String serviceUrl) {
logger.warn("All retries exhausted for {}. Using fallback.", serviceUrl);
return "Fallback response for " + serviceUrl;
}
@Recover
public String recover(TimeoutException e, String serviceUrl) {
logger.warn("Timeout retries exhausted for {}. Using timeout fallback.", serviceUrl);
return "Timeout fallback for " + serviceUrl;
}
/**
* Programmatic retry using RetryTemplate
*/
public String callServiceWithTemplate(String serviceUrl) {
return retryTemplate.execute(context -> {
int attempt = context.getRetryCount() + 1;
logger.info("Template-based call attempt {} for {}", attempt, serviceUrl);
// Simulate service call
if (Math.random() < 0.7) {
throw new IllegalStateException("Service busy on attempt " + attempt);
}
return "Success from " + serviceUrl + " on attempt " + attempt;
});
}
/**
* Retry with custom condition
*/
public String callServiceWithCustomRetry(String serviceUrl) {
return retryTemplate.execute(context -> {
int attempt = context.getRetryCount() + 1;
// Custom retry logic based on attempt number
if (attempt > 2) {
// After 2 attempts, change strategy
logger.info("Using alternative strategy on attempt {}", attempt);
}
// Simulate success on later attempts
if (attempt < 3 && Math.random() < 0.8) {
throw new TimeoutException("Simulated timeout");
}
return "Custom retry success on attempt " + attempt;
});
}
}
Circuit Breaker with Retry Pattern
Circuit Breaker Implementation
package com.example.retry.circuitbreaker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public class CircuitBreaker {
private static final Logger logger = LoggerFactory.getLogger(CircuitBreaker.class);
private final String name;
private final int failureThreshold;
private final Duration timeout;
private final AtomicInteger failureCount;
private final AtomicReference<State> state;
private final AtomicReference<Instant> lastFailureTime;
public CircuitBreaker(String name, int failureThreshold, Duration timeout) {
this.name = name;
this.failureThreshold = failureThreshold;
this.timeout = timeout;
this.failureCount = new AtomicInteger(0);
this.state = new AtomicReference<>(State.CLOSED);
this.lastFailureTime = new AtomicReference<>(Instant.now());
}
public <T> T execute(Callable<T> operation) throws Exception {
if (state.get() == State.OPEN) {
if (isTimeoutElapsed()) {
logger.info("Circuit breaker {} timeout elapsed, transitioning to HALF_OPEN", name);
state.set(State.HALF_OPEN);
} else {
logger.warn("Circuit breaker {} is OPEN, failing fast", name);
throw new CircuitBreakerOpenException("Circuit breaker is open");
}
}
try {
T result = operation.call();
onSuccess();
return result;
} catch (Exception e) {
onFailure();
throw e;
}
}
private void onSuccess() {
failureCount.set(0);
if (state.get() == State.HALF_OPEN) {
logger.info("Circuit breaker {} successful in HALF_OPEN state, transitioning to CLOSED", name);
state.set(State.CLOSED);
}
}
private void onFailure() {
int failures = failureCount.incrementAndGet();
lastFailureTime.set(Instant.now());
logger.warn("Circuit breaker {} failure count: {}/{}", name, failures, failureThreshold);
if (failures >= failureThreshold && state.get() != State.OPEN) {
logger.warn("Circuit breaker {} threshold exceeded, transitioning to OPEN", name);
state.set(State.OPEN);
}
}
private boolean isTimeoutElapsed() {
return Duration.between(lastFailureTime.get(), Instant.now()).compareTo(timeout) > 0;
}
public State getState() {
return state.get();
}
public int getFailureCount() {
return failureCount.get();
}
public void reset() {
failureCount.set(0);
state.set(State.CLOSED);
lastFailureTime.set(Instant.now());
logger.info("Circuit breaker {} manually reset", name);
}
public enum State {
CLOSED, // Normal operation
OPEN, // Failing fast
HALF_OPEN // Testing if service recovered
}
public static class CircuitBreakerOpenException extends RuntimeException {
public CircuitBreakerOpenException(String message) {
super(message);
}
}
}
Circuit Breaker with Retry
package com.example.retry.circuitbreaker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.concurrent.Callable;
import java.util.function.Predicate;
public class CircuitBreakerWithRetry {
private static final Logger logger = LoggerFactory.getLogger(CircuitBreakerWithRetry.class);
private final CircuitBreaker circuitBreaker;
private final int maxRetries;
private final Duration retryDelay;
private final Predicate<Exception> retryCondition;
public CircuitBreakerWithRetry(String name, int failureThreshold, Duration circuitTimeout,
int maxRetries, Duration retryDelay) {
this(name, failureThreshold, circuitTimeout, maxRetries, retryDelay, ex -> true);
}
public CircuitBreakerWithRetry(String name, int failureThreshold, Duration circuitTimeout,
int maxRetries, Duration retryDelay,
Predicate<Exception> retryCondition) {
this.circuitBreaker = new CircuitBreaker(name, failureThreshold, circuitTimeout);
this.maxRetries = maxRetries;
this.retryDelay = retryDelay;
this.retryCondition = retryCondition;
}
public <T> T execute(Callable<T> operation) throws Exception {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
return circuitBreaker.execute(operation);
} catch (CircuitBreaker.CircuitBreakerOpenException e) {
logger.warn("Circuit breaker open on attempt {}/{}", attempt, maxRetries);
if (attempt == maxRetries) {
throw e;
}
} catch (Exception e) {
if (!retryCondition.test(e) || attempt == maxRetries) {
throw e;
}
logger.warn("Retryable exception on attempt {}/{}: {}",
attempt, maxRetries, e.getMessage());
}
if (attempt < maxRetries) {
Thread.sleep(retryDelay.toMillis());
}
}
throw new RuntimeException("All retry attempts exhausted");
}
public CircuitBreaker.State getCircuitState() {
return circuitBreaker.getState();
}
public void resetCircuit() {
circuitBreaker.reset();
}
// Builder for fluent configuration
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String name = "default";
private int failureThreshold = 5;
private Duration circuitTimeout = Duration.ofSeconds(30);
private int maxRetries = 3;
private Duration retryDelay = Duration.ofSeconds(1);
private Predicate<Exception> retryCondition = ex -> true;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder failureThreshold(int failureThreshold) {
this.failureThreshold = failureThreshold;
return this;
}
public Builder circuitTimeout(Duration circuitTimeout) {
this.circuitTimeout = circuitTimeout;
return this;
}
public Builder maxRetries(int maxRetries) {
this.maxRetries = maxRetries;
return this;
}
public Builder retryDelay(Duration retryDelay) {
this.retryDelay = retryDelay;
return this;
}
public Builder retryWhen(Predicate<Exception> retryCondition) {
this.retryCondition = retryCondition;
return this;
}
public CircuitBreakerWithRetry build() {
return new CircuitBreakerWithRetry(
name, failureThreshold, circuitTimeout,
maxRetries, retryDelay, retryCondition
);
}
}
}
Advanced Retry Strategies
Retry with Exponential Backoff and Jitter
package com.example.retry.strategy;
import java.time.Duration;
import java.util.Random;
import java.util.concurrent.Callable;
public class ExponentialBackoffWithJitter {
private final int maxAttempts;
private final Duration initialDelay;
private final double multiplier;
private final Duration maxDelay;
private final double jitterFactor;
private final Random random;
public ExponentialBackoffWithJitter(int maxAttempts, Duration initialDelay,
double multiplier, Duration maxDelay,
double jitterFactor) {
this.maxAttempts = maxAttempts;
this.initialDelay = initialDelay;
this.multiplier = multiplier;
this.maxDelay = maxDelay;
this.jitterFactor = jitterFactor;
this.random = new Random();
}
public <T> T execute(Callable<T> operation) throws Exception {
Exception lastException = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return operation.call();
} catch (Exception e) {
lastException = e;
if (attempt == maxAttempts) {
throw lastException;
}
long delay = calculateDelayWithJitter(attempt);
Thread.sleep(delay);
}
}
throw lastException;
}
private long calculateDelayWithJitter(int attempt) {
long baseDelay = (long) (initialDelay.toMillis() * Math.pow(multiplier, attempt - 1));
long cappedDelay = Math.min(baseDelay, maxDelay.toMillis());
// Add jitter: random value between (1 - jitterFactor) * delay and (1 + jitterFactor) * delay
double jitter = 1 + (random.nextDouble() * 2 * jitterFactor - jitterFactor);
long jitteredDelay = (long) (cappedDelay * jitter);
return Math.max(initialDelay.toMillis(), jitteredDelay); // Ensure at least initial delay
}
public static ExponentialBackoffWithJitter createDefault() {
return new ExponentialBackoffWithJitter(
5, Duration.ofSeconds(1), 2.0, Duration.ofSeconds(30), 0.1
);
}
}
Retry with Resource-specific Strategies
package com.example.retry.strategy;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
public class ResourceAwareRetryManager {
private final Map<String, RetryStrategy> resourceStrategies;
private final RetryStrategy defaultStrategy;
public ResourceAwareRetryManager(RetryStrategy defaultStrategy) {
this.resourceStrategies = new HashMap<>();
this.defaultStrategy = defaultStrategy;
}
public void registerStrategy(String resource, RetryStrategy strategy) {
resourceStrategies.put(resource, strategy);
}
public <T> T execute(String resource, Callable<T> operation) throws Exception {
RetryStrategy strategy = resourceStrategies.getOrDefault(resource, defaultStrategy);
return strategy.execute(operation);
}
public interface RetryStrategy {
<T> T execute(Callable<T> operation) throws Exception;
}
public static class FixedRetryStrategy implements RetryStrategy {
private final int maxAttempts;
private final Duration delay;
public FixedRetryStrategy(int maxAttempts, Duration delay) {
this.maxAttempts = maxAttempts;
this.delay = delay;
}
@Override
public <T> T execute(Callable<T> operation) throws Exception {
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return operation.call();
} catch (Exception e) {
if (attempt == maxAttempts) throw e;
Thread.sleep(delay.toMillis());
}
}
throw new RuntimeException("Should not reach here");
}
}
}
Testing Retry Implementations
Unit Tests
package com.example.retry.test;
import com.example.retry.SimpleRetry;
import com.example.retry.circuitbreaker.CircuitBreakerWithRetry;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
class RetryPatternTest {
@Test
void testSimpleRetrySuccess() throws Exception {
AtomicInteger callCount = new AtomicInteger(0);
SimpleRetry retry = SimpleRetry.builder()
.maxAttempts(3)
.delayMs(100)
.build();
String result = retry.execute(() -> {
callCount.incrementAndGet();
if (callCount.get() < 2) {
throw new IOException("Temporary failure");
}
return "Success";
});
assertEquals("Success", result);
assertEquals(2, callCount.get());
}
@Test
void testSimpleRetryFailure() {
AtomicInteger callCount = new AtomicInteger(0);
SimpleRetry retry = SimpleRetry.builder()
.maxAttempts(3)
.delayMs(100)
.build();
assertThrows(IOException.class, () -> {
retry.execute(() -> {
callCount.incrementAndGet();
throw new IOException("Persistent failure");
});
});
assertEquals(3, callCount.get());
}
@Test
void testCircuitBreakerWithRetry() throws Exception {
AtomicInteger callCount = new AtomicInteger(0);
CircuitBreakerWithRetry cbRetry = CircuitBreakerWithRetry.builder()
.name("test-service")
.failureThreshold(2)
.circuitTimeout(Duration.ofSeconds(1))
.maxRetries(3)
.retryDelay(Duration.ofMillis(100))
.build();
// First call succeeds
String result1 = cbRetry.execute(() -> {
callCount.incrementAndGet();
return "Success";
});
assertEquals("Success", result1);
assertEquals(1, callCount.get());
// Simulate failures to open circuit breaker
assertThrows(IOException.class, () -> {
cbRetry.execute(() -> {
callCount.incrementAndGet();
throw new IOException("Service down");
});
});
// Circuit should be open now
assertEquals(CircuitBreakerWithRetry.CircuitBreaker.State.OPEN,
cbRetry.getCircuitState());
}
@Test
void testRetryWithCondition() throws Exception {
AtomicInteger callCount = new AtomicInteger(0);
SimpleRetry retry = SimpleRetry.builder()
.maxAttempts(3)
.delayMs(100)
.retryOn(ex -> ex.getMessage().contains("retry"))
.build();
// This should retry
assertThrows(IOException.class, () -> {
retry.execute(() -> {
callCount.incrementAndGet();
throw new IOException("Please retry this operation");
});
});
assertEquals(3, callCount.get());
// Reset counter
callCount.set(0);
// This should not retry
assertThrows(IOException.class, () -> {
retry.execute(() -> {
callCount.incrementAndGet();
throw new IOException("Fatal error");
});
});
assertEquals(1, callCount.get());
}
}
Real-world Usage Examples
Database Operation with Retry
package com.example.retry.examples;
import com.example.retry.RetryTemplate;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.TransientDataAccessException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Duration;
public class DatabaseService {
private final RetryTemplate<String> dbRetry;
public DatabaseService() {
this.dbRetry = RetryTemplate.<String>builder()
.maxAttempts(5)
.initialDelay(Duration.ofSeconds(1))
.exponentialBackoff(2.0, Duration.ofSeconds(10))
.retryOn(TransientDataAccessException.class, SQLException.class)
.notRetryOn(DataAccessException.class) // Non-transient exceptions
.onRetry(context -> {
System.out.printf("Database retry %d for %s%n",
context.getAttempt(), context.getLastException().getMessage());
})
.build();
}
public String queryWithRetry(String sql) throws Exception {
return dbRetry.execute(() -> {
try (Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return rs.getString(1);
}
return null;
} catch (SQLException e) {
if (isTransientError(e)) {
throw new TransientDataAccessException("Transient database error", e) {};
}
throw e;
}
});
}
private boolean isTransientError(SQLException e) {
// SQL State for connection failures, timeouts, etc.
return "08001".equals(e.getSQLState()) ||
"08004".equals(e.getSQLState()) ||
"08007".equals(e.getSQLState()) ||
e.getMessage().contains("timeout");
}
private Connection getConnection() throws SQLException {
// Implementation to get database connection
return null;
}
}
HTTP Client with Retry
package com.example.retry.examples;
import com.example.retry.circuitbreaker.CircuitBreakerWithRetry;
import org.springframework.http.HttpStatus;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
import java.util.Map;
public class HttpClientWithRetry {
private final RestTemplate restTemplate;
private final CircuitBreakerWithRetry circuitBreaker;
public HttpClientWithRetry(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
this.circuitBreaker = CircuitBreakerWithRetry.builder()
.name("http-client")
.failureThreshold(5)
.circuitTimeout(Duration.ofSeconds(60))
.maxRetries(3)
.retryDelay(Duration.ofSeconds(2))
.retryWhen(ex -> isRetryableException(ex))
.build();
}
public <T> T getWithRetry(String url, Class<T> responseType) {
try {
return circuitBreaker.execute(() -> {
return restTemplate.getForObject(url, responseType);
});
} catch (Exception e) {
throw new RuntimeException("HTTP request failed after retries", e);
}
}
public <T> T postWithRetry(String url, Object request, Class<T> responseType) {
try {
return circuitBreaker.execute(() -> {
return restTemplate.postForObject(url, request, responseType);
});
} catch (Exception e) {
throw new RuntimeException("HTTP POST failed after retries", e);
}
}
private boolean isRetryableException(Exception ex) {
return ex instanceof ResourceAccessException || // Connection timeout, etc.
(ex instanceof HttpServerErrorException &&
((HttpServerErrorException) ex).getStatusCode().is5xxServerError()) ||
(ex.getMessage() != null &&
(ex.getMessage().contains("timeout") ||
ex.getMessage().contains("unavailable")));
}
public void resetCircuit() {
circuitBreaker.resetCircuit();
}
public CircuitBreakerWithRetry.CircuitBreaker.State getCircuitState() {
return circuitBreaker.getCircuitState();
}
}
Best Practices
- Identify Retryable Errors: Distinguish between transient and permanent failures
- Use Exponential Backoff: Prevent overwhelming the failing service
- Add Jitter: Avoid synchronized retry storms
- Set Reasonable Limits: Maximum attempts and timeout durations
- Monitor Retry Patterns: Track retry rates and success/failure metrics
- Consider Circuit Breakers: For cascading failure prevention
- Test Thoroughly: Verify retry behavior under different scenarios
- Log Appropriately: Record retry attempts without being too verbose
- Use Context Information: Include attempt numbers in error messages
- Consider Idempotency: Ensure retried operations are safe to repeat
This comprehensive retry pattern implementation provides robust error handling for distributed systems and unreliable operations, with configurable strategies, circuit breaker integration, and production-ready features.