Spring Batch — Retry Logic Deep Dive

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.

ApproachWhere to useProsCons
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"
}

๐Ÿ”— Related Posts