Repeat & retry internals

This section documents the current Spring Batch line — 6.0.x, on Spring Framework 7 and Spring Boot 4.1.x, with a Java 17+ baseline — as published at the Spring Batch reference documentation. No specific patch version is pinned. Some surfaces (Spring Cloud Task and Spring Cloud Data Flow orchestration, the deployer-based partition handler, and JSR-352) are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The chunk loop and the fault-tolerance options are conveniences over two general-purpose abstractions: RepeatOperations, which runs something repeatedly until a policy says stop, and RetryOperations, which runs something again after a failure. Both are usable on their own.

RepeatOperations

public interface RepeatOperations {
    RepeatStatus iterate(RepeatCallback callback) throws RepeatException;
}

RepeatTemplate is the implementation. It calls the callback in a loop, passing a RepeatContext, and stops when the callback returns RepeatStatus.FINISHED or the CompletionPolicy says the batch is complete:

RepeatTemplate template = new RepeatTemplate();
template.setCompletionPolicy(new SimpleCompletionPolicy(100));   // at most 100 iterations
template.setExceptionHandler(new SimpleLimitExceptionHandler(5)); // tolerate 5 failures, then rethrow

template.iterate(context -> {
    Trade trade = tradeQueue.poll();
    if (trade == null) {
        return RepeatStatus.FINISHED;
    }
    tradeService.settle(trade);
    return RepeatStatus.CONTINUABLE;
});

This is exactly the shape of the chunk loop in Chunk-oriented processing: "read and process until the completion policy says the chunk is full".

CompletionPolicy

Policy Stops the batch when

SimpleCompletionPolicy

A fixed number of iterations has run. This is what an integer chunk size creates.

TimeoutTerminationPolicy

A wall-clock budget has elapsed — the last iteration is allowed to finish.

CountingCompletionPolicy

A counter derived from the result (bytes, records, cost) reaches a limit.

CompositeCompletionPolicy

Any (or all) of several policies is satisfied.

A custom policy is a small class, and it is how "commit whenever the business key changes" is expressed:

public class KeyChangeCompletionPolicy extends CompletionPolicySupport {

    private String currentKey;

    @Override
    public boolean isComplete(RepeatContext context, RepeatStatus status) {
        return status == RepeatStatus.FINISHED;
    }

    @Override
    public void update(RepeatContext context) {
        String key = (String) context.getAttribute("account.key");
        if (currentKey != null && !currentKey.equals(key)) {
            context.setCompleteOnly();     // close the chunk at the key boundary
        }
        currentKey = key;
    }
}

ExceptionHandler

The ExceptionHandler decides whether an exception thrown inside an iteration ends the loop. SimpleLimitExceptionHandler rethrows only after N occurrences — the mechanism behind a step’s skip limit. LogOrRethrowExceptionHandler classifies exceptions into "log and continue" and "rethrow".

RepeatTemplate can also be given a TaskExecutor (TaskExecutorRepeatTemplate) to run iterations concurrently — the basis of the multi-threaded step in Scaling & parallel processing.

RetryOperations

Retry is the other half. Spring Framework 7 ships its own core retry support — the types live in org.springframework.core.retry, and this is what the newer ChunkOrientedStepBuilder uses. Note that the .faultTolerant() builder is still on the Spring Retry library (Fault tolerance: skip & retry), so the policy below applies to the former, not the latter:

RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(RetryPolicy.builder()
        .maxAttempts(3)
        .includes(TransientDataAccessException.class, RemoteAccessException.class)
        .excludes(DataIntegrityViolationException.class)
        .delay(Duration.ofMillis(200))
        .multiplier(2.0)
        .maxDelay(Duration.ofSeconds(5))
        .build());

Trade settled = retryTemplate.execute(() -> settlementClient.settle(trade));

The pieces:

  • RetryPolicy — how many attempts, which exceptions qualify, and the delay between attempts. A custom policy can inspect the exception and the attempt count.

  • BackOffPolicy — fixed, exponential or exponential-with-jitter delays. Retrying a contended resource without back-off usually reproduces the same failure; jitter additionally prevents a thundering herd when many workers fail together.

  • RetryListener — callbacks on each failed attempt and on exhaustion; the natural place for logging and metrics (Observability).

  • Recovery — a fallback invoked when the attempts are exhausted, so the caller gets a degraded result instead of an exception.

Stateless vs. stateful retry

Behaviour

Stateless

All attempts happen inside one call, in a loop, on the same thread and inside the same transaction. Right for a failure that does not poison the transaction — a flaky remote call, a read timeout.

Stateful

The failure aborts the transaction; the retry happens on a subsequent call, with the attempt count keyed by an item identity and stored in a retry context. Right for a database failure that marks the transaction rollback-only, such as a deadlock — there is no point retrying inside a transaction that can no longer commit.

This distinction is why a fault-tolerant chunk step retries the way it does: because the chunk’s transaction has already rolled back, the framework replays the chunk in a new transaction and keys the retry state by item — stateful retry, applied automatically.

How a fault-tolerant step maps onto these

Step-level setting Underlying mechanism

.chunk(500, tx)

A RepeatTemplate with a SimpleCompletionPolicy(500) driving read-and-process.

.chunk(policy, tx)

The same, with the supplied CompletionPolicy.

.retry(X.class).retryLimit(3)

A RetryPolicy limited to three attempts, applied statefully around the write (and the process, if it is transactional).

.backOffPolicy(…​)

The BackOffPolicy handed to that retry policy.

.skip(X.class).skipLimit(50)

An ExceptionHandler/SkipPolicy pair that tolerates 50 occurrences before rethrowing.

.noRollback(X.class)

A rollback classifier consulted before the transaction is marked rollback-only.

Nearly all batch code should stay at the step level — .faultTolerant().skip(…​).retry(…​) as described in Fault tolerance: skip & retry. Reach for RetryTemplate or RepeatTemplate directly only inside a tasklet, a custom reader or writer, or a service that is called from batch and online code alike.

Further reading

For the full detail behind this page: