skip to content

How is a step's ExitStatus derived from its BatchStatus by default, and how do you customize ExitStatus?

level: middleimportance: must knowfreq 45%

answer

  1. afterStep returns ExitStatus; null = keep default
  2. Default: exitCode == BatchStatus name
  3. Combine with ExitStatus.and(...) to keep severity
  4. Custom code -> branching key
  5. Changing ExitStatus doesn't change BatchStatus

basics

~10 s

By default the ExitStatus mirrors the BatchStatus (COMPLETED->'COMPLETED', FAILED->'FAILED'). To customize, return a different ExitStatus from a StepExecutionListener's afterStep method (or set it on the StepExecution). The BatchStatus itself stays unchanged.

solid answer

~40 s

When a step finishes, Spring Batch sets a BatchStatus from the technical outcome and then derives a matching ExitStatus (e.g. COMPLETED -> ExitStatus.COMPLETED, FAILED -> ExitStatus.FAILED). To override the exit code you implement a StepExecutionListener and return a new ExitStatus from afterStep(StepExecution); returning null keeps the default. A common pattern is to inspect the StepExecution (skip counts, read/write counts) and return a custom code like 'COMPLETED_WITH_SKIPS' so a later flow decision can branch on it. Importantly, changing the ExitStatus does not change the BatchStatus — the framework still records COMPLETED/FAILED. You typically combine the existing exit status with the new one via ExitStatus.and(...) to preserve severity. This lets you enrich the outcome code without lying about the lifecycle state.

code

java · 21 lines
java
public class SkipAwareListener implements StepExecutionListener {

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        // Only enrich a successful step; preserve severity via and(...)
        if (stepExecution.getStatus() == BatchStatus.COMPLETED
                && stepExecution.getSkipCount() > 0) {
            return stepExecution.getExitStatus()
                    .and(new ExitStatus("COMPLETED_WITH_SKIPS",
                            stepExecution.getSkipCount() + " records skipped"));
        }
        return null; // keep the default ExitStatus
    }
}

// wiring
Step step = new StepBuilder("load", jobRepository)
        .<In, Out>chunk(100, txManager)
        .reader(reader).processor(processor).writer(writer)
        .listener(new SkipAwareListener())
        .build();

go deeper

for a junior

Knows default codes mirror BatchStatus and that afterStep can override.

for a middle

Must show the afterStep listener pattern, null-to-keep-default, and independence from BatchStatus.

for a senior

Explains and()/severity preservation and the failure-masking gotcha.

for a principal

Discusses when custom exit codes are the right seam vs. abusing them, and reporting/observability implications.

## Default derivation At the end of a `Step`, Spring Batch first determines the **BatchStatus** (`COMPLETED`, `FAILED`, `STOPPED`...) from what actually happened. It then produces a matching **ExitStatus**: the default `ExitStatus` has an `exitCode` equal to the BatchStatus name — `COMPLETED` -> `ExitStatus.COMPLETED`, `FAILED` -> `ExitStatus.FAILED`, `STOPPED` -> `ExitStatus.STOPPED`. So with no customization, the exit code and lifecycle agree. ## Customizing via StepExecutionListener The canonical hook is `StepExecutionListener.afterStep(StepExecution stepExecution)`, which **returns an `ExitStatus`**: - Return a **new ExitStatus** to override the exit code. - Return **null** to leave the default in place. You usually **combine** rather than replace, using `ExitStatus.and(ExitStatus)`, which keeps the more severe code and concatenates descriptions. That way a failing step's severity is not accidentally downgraded. ```java @Override public ExitStatus afterStep(StepExecution stepExecution) { if (stepExecution.getSkipCount() > 0) { return stepExecution.getExitStatus() .and(new ExitStatus("COMPLETED_WITH_SKIPS")); } return null; // keep default } ``` ## Key rule: ExitStatus change ≠ BatchStatus change Returning a custom ExitStatus from `afterStep` alters only the **exit code**. The framework has already decided the BatchStatus independently and persists it in `BATCH_STEP_EXECUTION.STATUS`; your custom exit code lands in `BATCH_STEP_EXECUTION.EXIT_CODE`. So you can have `BatchStatus.COMPLETED` alongside `ExitStatus('COMPLETED_WITH_SKIPS')`. ## Where the custom code is used The custom exit code becomes the **key that flow transitions match on** (the `on("COMPLETED_WITH_SKIPS")` style routing — the details of transition wiring belong to flow-control). It is also visible to monitoring/reporting. ## Registration Register the listener on the step (`stepBuilder.listener(myListener)`). A bean implementing `StepExecutionListener`, or a POJO annotated with `@AfterStep`, both work. `@AfterStep`-annotated methods must return `ExitStatus` to influence it. ## Gotchas - Forgetting to combine with `getExitStatus()` can **overwrite** a FAILED exit code and mask a failure. - If `afterStep` throws, the step is marked FAILED. - Returning a custom code does **not** retroactively mark the step failed — if you want failure, you must let the step fail (throw) or set BatchStatus via other means; you cannot force FAILED just by returning ExitStatus.FAILED from afterStep (it changes only the code, not the persisted BatchStatus).

  • Why combine with ExitStatus.and(...) instead of just returning new ExitStatus("...")?
    and() keeps the more severe of the two exit codes and merges descriptions. If the step actually failed, blindly returning a COMPLETED-style custom code would downgrade/mask the failure. Combining preserves the existing severity.
  • If afterStep returns ExitStatus.FAILED, is the step's BatchStatus now FAILED?
    No. Returning an exit code does not change the persisted BatchStatus. To make a step FAILED you must actually let it fail (throw an exception). afterStep only shapes the exit code.

saying these in an interview costs you the question

  • Believing returning ExitStatus.FAILED from afterStep marks the step BatchStatus.FAILED.
  • Overwriting the exit status without preserving severity, masking failures.
  • Thinking you must edit the database to change the exit code.

context