Spring Batch — Retry Logic Deep Dive
Retries are a fundamental technique for building resilient batch processes. Some failures are transient (network blips, temporary DB locks, remote API hiccups) and recoverable by retrying. This guide explains when and how to use retries in Spring Batch, configuration patterns, backoff strategies, listeners and metrics, pitfalls like non-idempotency and transaction boundaries, and production best practices.
๐ Why Retry?
- Transient infrastructure errors (DB deadlocks, network timeouts)
- Intermittent external API failures
- Short-lived resource contention
Rule of thumb: Retry for transient problems, not for deterministic data validation errors. If a retry won't change the outcome, don’t retry — skip or fail instead.
๐ง Two Ways to Retry in Spring Batch
Spring Batch supports retry at the step level using the faultTolerant() builder and also via Spring Retry (e.g. RetryTemplate) for custom code paths.
| Approach | Where to use | Pros | Cons |
|---|---|---|---|
| Spring Batch faultTolerant().retry() | Reader / Processor / Writer failures in chunk steps | Integrated with chunk semantics, skip/retry metrics, listeners | Less flexible than RetryTemplate for ad-hoc retries |
| Spring Retry / RetryTemplate | Custom service calls inside processor/writer or outside Batch | Very flexible: policies, backoff, recoverers | Requires manual integration with Batch semantics |
⚙️ Example: Retry Configuration with faultTolerant()
@Bean
public Step chunkStep() {
return stepBuilderFactory.get("chunkStep")
.chunk(10)
.reader(reader())
.processor(processor())
.writer(writer())
.faultTolerant()
.retry(TransientDataAccessException.class) // retry on these exceptions
.retryLimit(3) // max attempts
.backOffPolicy(new FixedBackOffPolicy() {{ setBackOffPeriod(2000L); }})
.retryPolicy(new SimpleRetryPolicy(3, Collections.singletonMap(SomeRemoteException.class, true)))
.build();
}
⏱ Backoff Strategies
ExponentialBackOffPolicy backoff = new ExponentialBackOffPolicy(); backoff.setInitialInterval(500L); backoff.setMultiplier(2.0); backoff.setMaxInterval(10_000L); faultTolerant() .retry(SomeTransientException.class) .backOffPolicy(backoff);
๐ Stateful vs Stateless Retry
/* Enable stateful retry */ // In Spring Retry you can use StatefulRetryOperationsInterceptor and configure a key RetryTemplate retryTemplate = new RetryTemplate(); // configure stateful policy / recovery
๐งฉ RetryListener Example
public class LoggingRetryListener implements RetryListener {
@Override
public boolean open(RetryContext context, RetryCallback callback) {
return true;
}
@Override
public void close(RetryContext context, RetryCallback callback, Throwable throwable) {
// After all retry attempts
}
@Override
public void onError(RetryContext context, RetryCallback callback, Throwable throwable) {
System.out.println("Retry attempt: " + context.getRetryCount() + " due to " + throwable.getMessage());
}
}
๐ RetryTemplate Example
RetryTemplate template = new RetryTemplate();
SimpleRetryPolicy policy = new SimpleRetryPolicy(3);
template.setRetryPolicy(policy);
ExponentialBackOffPolicy backoff = new ExponentialBackOffPolicy();
backoff.setInitialInterval(500);
backoff.setMultiplier(2.0);
template.setBackOffPolicy(backoff);
String result = template.execute(context -> {
return remoteService.call();
}, context -> {
return "fallback";
});
๐งช Testing Retry Logic
@Test
void testRetryOnTransientException() {
when(remoteService.call())
.thenThrow(new TransientException())
.thenThrow(new TransientException())
.thenReturn("OK");
// assert result == "OK"
}