Fault Tolerance, Skip, and Retry Policies
Configure skip, retry, and restart semantics to make jobs resilient to transient data errors.
Fault Tolerance, Skip, and Retry Policies is a free Spring Boot 4 Complete Guide lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Spring Boot 4 Complete Guide learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Fault Tolerance Matters
Batch jobs process huge volumes of data, and real-world data is messy. A single malformed row, a momentary database deadlock, or a flaky downstream call can blow up a job that has already processed millions of records.
Spring Batch gives a chunk-oriented step fault tolerance so it can survive these issues without aborting the whole run. The three core tools are:
- Skip — discard records that cause unrecoverable errors (e.g. bad data) and keep going.
- Retry — re-attempt an operation that failed due to a transient error (e.g. a lock timeout).
- Restart — resume a failed job instance from where it stopped instead of starting over.
Used together, these turn a brittle job into a resilient one.
Enabling Fault Tolerance on a Step
Fault tolerance is opt-in. When you build a chunk step, call .faultTolerant() on the step builder to switch to the fault-tolerant variant. Only then can you declare skip and retry rules.
Without .faultTolerant(), any exception thrown by a reader, processor, or writer rolls back the chunk and fails the step immediately.
@Bean
public Step importStep(JobRepository jobRepository,
PlatformTransactionManager txManager,
ItemReader<Customer> reader,
ItemProcessor<Customer, Customer> processor,
ItemWriter<Customer> writer) {
return new StepBuilder("importStep", jobRepository)
.<Customer, Customer>chunk(100, txManager)
.reader(reader)
.processor(processor)
.writer(writer)
.faultTolerant() // unlocks skip & retry configuration
.build();
}Configuring Skip Policies
Skipping lets the step throw away an individual item that can never succeed — typically a parsing or validation failure — and continue with the next one.
You declare which exceptions are skippable and a global limit:
.skip(Exception.class)— mark an exception type as skippable..noSkip(Exception.class)— explicitly exclude a subtype from skipping..skipLimit(n)— total number of skips allowed before the step fails.
Once the cumulative skip count exceeds skipLimit, the step aborts. This prevents a job from silently swallowing thousands of bad records.
return new StepBuilder("importStep", jobRepository)
.<Customer, Customer>chunk(100, txManager)
.reader(reader)
.processor(processor)
.writer(writer)
.faultTolerant()
.skip(FlatFileParseException.class) // bad CSV line
.skip(ValidationException.class) // failed bean validation
.noSkip(FileNotFoundException.class) // never skip this
.skipLimit(50) // fail after 50 skips
.build();How Skip Interacts with Chunks
Skip behaviour depends on where the exception is thrown:
- Reader skip: the bad item is dropped and reading continues — cheap, no rollback.
- Processor skip: the chunk transaction rolls back, then Spring Batch re-processes the chunk item-by-item, skipping only the offending item.
- Writer skip: same scan-and-retry — the chunk rolls back and items are re-written one at a time so the single bad item can be isolated and skipped.
Because processor/writer skips trigger a rollback and a single-item replay, they are far more expensive than reader skips. Keep validation in the reader/processor where possible so failures are caught early.
A Custom SkipPolicy
The declarative .skip()/.skipLimit() API covers most cases, but you can implement SkipPolicy for fully custom logic — for example, allow more skips for one exception type than another, or inspect the exception message.
The shouldSkip method receives the thrown Throwable and the current skip count; return true to skip, or throw SkipLimitExceededException to fail the step.
public class CustomSkipPolicy implements SkipPolicy {
@Override
public boolean shouldSkip(Throwable t, long skipCount)
throws SkipLimitExceededException {
if (t instanceof FileNotFoundException) {
return false; // fatal: never skip
}
if (t instanceof ValidationException && skipCount < 100) {
return true; // tolerate up to 100 bad records
}
if (t instanceof FlatFileParseException && skipCount < 20) {
return true;
}
return false;
}
}When to Retry vs Skip
The decision between skip and retry comes down to the nature of the error:
- Retry a transient error that may succeed if attempted again: deadlock victim, lock timeout, optimistic locking conflict, brief network blip.
- Skip a deterministic error that will always fail: malformed input, a failed business validation, a constraint that the data itself violates.
Retrying a deterministic error just wastes attempts before failing; skipping a transient error throws away data that would have succeeded. Classify your exceptions correctly — this is the key design decision of the lesson.
Configuring Retry Policies
Retry re-attempts the failing operation up to a configured number of times before giving up. On a fault-tolerant step you declare:
.retry(Exception.class)— exception types that are retryable..noRetry(Exception.class)— exclude a subtype..retryLimit(n)— maximum attempts per item (including the first try).
When an item fails with a retryable exception, the chunk transaction rolls back and the item is replayed up to retryLimit times. If it still fails, the exception propagates — at which point it may be skipped if it is also declared skippable.
return new StepBuilder("importStep", jobRepository)
.<Customer, Customer>chunk(100, txManager)
.reader(reader)
.processor(processor)
.writer(writer)
.faultTolerant()
.retry(DeadlockLoserDataAccessException.class)
.retry(OptimisticLockingFailureException.class)
.retryLimit(3) // up to 3 attempts per item
.skip(ValidationException.class)
.skipLimit(50)
.build();Backoff Between Retries
Hammering a contended resource with immediate retries often makes contention worse. A backoff policy inserts a delay between attempts. ExponentialBackOffPolicy grows the wait multiplicatively, spreading out load.
You attach a custom RetryPolicy or BackOffPolicy via .retryPolicy(...) / by configuring a RetryTemplate. Below, the wait starts at 200ms and doubles each attempt up to 5s.
@Bean
public RetryTemplate retryTemplate() {
ExponentialBackOffPolicy backOff = new ExponentialBackOffPolicy();
backOff.setInitialInterval(200); // 200 ms
backOff.setMultiplier(2.0); // 200, 400, 800, ...
backOff.setMaxInterval(5000); // cap at 5 s
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(3,
Map.of(DeadlockLoserDataAccessException.class, true));
RetryTemplate template = new RetryTemplate();
template.setBackOffPolicy(backOff);
template.setRetryPolicy(retryPolicy);
return template;
}Listeners: Observing Skips and Retries
Silently skipping records is dangerous — you need an audit trail. SkipListener callbacks fire for each skipped item so you can log it, write it to a dead-letter table, or alert.
onSkipInRead— a read failure was skipped.onSkipInProcess— gives you the item and the exception.onSkipInWrite— the item that could not be written.
Register it with .listener(skipListener) on the step builder. There is also RetryListener for observing retry attempts.
public class LoggingSkipListener implements SkipListener<Customer, Customer> {
private static final Logger log =
LoggerFactory.getLogger(LoggingSkipListener.class);
@Override
public void onSkipInRead(Throwable t) {
log.warn("Skipped unreadable record: {}", t.getMessage());
}
@Override
public void onSkipInProcess(Customer item, Throwable t) {
log.warn("Skipped {} in process: {}", item.getId(), t.getMessage());
}
@Override
public void onSkipInWrite(Customer item, Throwable t) {
log.warn("Skipped {} in write: {}", item.getId(), t.getMessage());
}
}Restartability and the Job Repository
Skip and retry handle errors within a run; restart handles a run that failed completely. Because Spring Batch persists each step's ExecutionContext and read/write counts in the job repository, relaunching the same JobInstance resumes from the last committed chunk rather than reprocessing everything.
Key rules:
- A
JobInstanceis identified by its identifying job parameters; reuse them to restart, change them to start a fresh instance. - Only jobs in a non-
COMPLETEDstate (e.g.FAILED,STOPPED) can be restarted. - Mark a step
.allowStartIfComplete(true)to force already-completed steps to re-run on restart. - Cap retries with
.startLimit(n)so a broken step is not relaunched forever.
Putting It All Together
A production-grade resilient step combines all three concerns: retry transient failures with backoff, skip deterministic bad data within a bounded limit, audit every skip, and rely on the job repository for restart.
Note the layering: an item that fails is first retried; if it still fails and the exception is skippable, it is skipped (and the listener records it). Exceptions can be both retryable and skippable — retry exhausts first, then skip applies.
return new StepBuilder("resilientImport", jobRepository)
.<Customer, Customer>chunk(100, txManager)
.reader(reader)
.processor(processor)
.writer(writer)
.faultTolerant()
// transient -> retry with backoff
.retry(DeadlockLoserDataAccessException.class)
.retryLimit(3)
// deterministic bad data -> skip
.skip(FlatFileParseException.class)
.skip(ValidationException.class)
.skipLimit(100)
// audit + restart safety
.listener(new LoggingSkipListener())
.startLimit(3)
.build();Quick Check
Test your understanding of skip vs. retry semantics.
Recap
You made a Spring Batch step resilient to transient and deterministic failures:
- Enable tolerance with
.faultTolerant()before declaring any skip/retry rules. - Skip deterministic bad data with
.skip()+.skipLimit(); processor/writer skips cost a rollback and single-item replay, so validate early. - Retry transient errors with
.retry()+.retryLimit(), adding an exponential backoff to ease contention. - Classify carefully: retry transient (deadlock, lock timeout), skip deterministic (parse/validation). When an exception is both, retry runs first, then skip.
- Audit every skip with a
SkipListenerso nothing disappears silently. - Restart failed instances from the last committed chunk via the job repository; control re-runs with
allowStartIfCompleteandstartLimit.
Frequently asked questions
Is the “Fault Tolerance, Skip, and Retry Policies” lesson free?
Yes — the full text of “Fault Tolerance, Skip, and Retry Policies” is free to read here on the web, and the Spring Boot 4 Complete Guide course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Spring Boot 4 Complete Guide course, upgrade to CoddyKit PRO.
What will I learn in “Fault Tolerance, Skip, and Retry Policies”?
Configure skip, retry, and restart semantics to make jobs resilient to transient data errors. You practise Spring Boot 4 Complete Guide with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Spring Boot 4 Complete Guide?
No prior experience is required. Spring Boot 4 Complete Guide on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Fault Tolerance, Skip, and Retry Policies” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Spring Boot 4 Complete Guide lesson?
Yes. Every Spring Boot 4 Complete Guide lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Jobs, Steps, and the JobRepository Model
- Chunk-Oriented Reader-Processor-Writer Flows
- Fault Tolerance, Skip, and Retry Policies
- Partitioning and Parallel Step Execution