# 8. Skip vs. restart, and the two knobs that shape a restart [← Previous](07-restartability.md) | [README](../README.md) | [Next: Corrections found while writing this →](09-corrections.md) Failing the job and restarting after a fix (chapter 7) is one answer to a bad row. It is the right one when the row is genuinely wrong and someone needs to decide what to do about it. It is the wrong one when the bad row is expected background noise — a handful of duplicates in a million-row feed — and stopping the whole job for each one is not realistic. The `skip` profile configures the same step with `faultTolerant().skip(...)` instead: ```java new StepBuilder("importStep", jobRepository) .chunk(10) .transactionManager(transactionManager) .reader(productReader) .processor(productProcessor) .writer(productWriter) .faultTolerant() .skip(DataIntegrityViolationException.class) .skipLimit(3) .listener(skipListener()) .build(); ``` Run against the same poisoned file used in chapter 7, in one pass, no restart needed: ```console $ SELECT status, read_count, filter_count, write_count, write_skip_count, commit_count, rollback_count FROM BATCH_STEP_EXECUTION; COMPLETED | 60 | 2 | 57 | 1 | 6 | 2 $ SELECT COUNT(*) FROM PRODUCT; COUNT(*) 57 ``` Full transcript: [`docs/output/10-skip-instead-of-fail.txt`](output/10-skip-instead-of-fail.txt). The job reads all 60 rows in one execution, filters the same 2 as always, skips exactly the duplicate (`WRITE_SKIP_COUNT` 1), and completes with 57 rows written. Note `ROLLBACK_COUNT` is 2, not 0: when a fault-tolerant chunk's batch write fails, Spring Batch does not give up on the chunk — it rolls back once, then re-processes that chunk's items **one at a time** to find out which single item is the actual problem, so it can skip only that one and keep the rest. That retry-by-scanning is one extra rollback for the chunk containing the bad row; you can see it in the count. ## `DataIntegrityViolationException.class` catches `DuplicateKeyException` because of the hierarchy The skip policy is registered against `DataIntegrityViolationException`, but the exception H2 actually throws is `DuplicateKeyException`. This works because `DuplicateKeyException extends DataIntegrityViolationException` — Spring's JDBC exception translation maps H2's `JdbcBatchUpdateException` (a unique-constraint violation) to the more specific subclass, and a skip policy registered against the parent still matches it. Confirmed directly, not assumed: ```console thrown type : org.springframework.dao.DuplicateKeyException is a DataIntegrityViolationException? true ``` (from [`docs/output/02-exception-hierarchy.txt`](output/02-exception-hierarchy.txt), produced by [`ProductValidatingProcessorTest.duplicateKeyExceptionIsADataIntegrityViolationException`](../src/test/java/com/ankurm/batch/ProductValidatingProcessorTest.java)). Registering skip policies against a broad parent type like `DataIntegrityViolationException` means you do not have to enumerate every specific subtype Spring's translation layer might produce — but it also means you are choosing to skip *any* data-integrity problem, not just duplicates. `skipLimit(3)` is the safety valve: past 3 skips in one execution, the step gives up and fails anyway, on the theory that "one bad row" and "the whole feed is corrupt" should not be handled identically. ## The two restart-shaping options on a step `reportStep` uses one of these; the football-job example in the Spring Batch reference documents both well, and this module's `reportStep` is a direct, smaller version of that pattern: - **`allowStartIfComplete(true)`** — by default, a step that already finished `COMPLETED` is skipped entirely on restart (the whole point of chapter 7 is not re-doing committed work). `reportStep` overrides that, because a status report should reflect the current state of the `PRODUCT` table on every run, including a restart, not just the first successful attempt. - **`startLimit(n)`** — not used in this module, but worth knowing: caps how many times a step may be *attempted* (not completed) before Spring Batch throws `StartLimitExceededException` instead of trying again. Useful for a step that does something non-idempotent enough that repeated automatic retries would make things worse — call out external systems, send notifications — where the right response to repeated failure is a human looking at it, not an unbounded retry loop. ## Going deeper - Skip and retry configuration in full, including `noSkip()` exclusions and combining skip with retry: [Spring Batch reference — configuring skip logic](https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing/configuring-skip.html) (`rel="nofollow"`). - `startLimit` and `allowStartIfComplete` with the football-job example this chapter borrows the shape of: [Spring Batch reference — configuring a step for restart](https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing/restart.html) (`rel="nofollow"`). [Next: Corrections found while writing this →](09-corrections.md)