skip to content

In Spring Batch, how do you make a job run a different step depending on whether the previous step succeeded or failed?

level: juniorimportance: must knowfreq 55%

answer

  1. on(pattern) matches ExitStatus string
  2. to() = destination, from() = re-anchor
  3. catch-all, ? one char
  4. cover every exit status or job errors
  5. end() closes the flow

basics

~10 s

Use conditional transitions on the job builder: .start(stepA).on("FAILED").to(cleanupStep).from(stepA).on("*").to(nextStep).end(). The on(...) pattern is matched against the previous step's ExitStatus to pick the next step.

solid answer

~40 s

Spring Batch models flow with conditional transitions. After a step you call `.on(pattern)` where the pattern is matched against that step's `ExitStatus` string (e.g. "COMPLETED", "FAILED"), then `.to(nextStep)` names where to go if it matches. `.from(step)` lets you attach additional transitions off the same step. So `job.start(stepA).on("FAILED").to(errorStep).from(stepA).on("*").to(happyStep).end()` sends the job to errorStep when stepA exits FAILED and to happyStep for any other exit status. The `*` wildcard is the catch-all. You must define enough transitions to cover every possible exit status, otherwise the job fails with an error about no matching transition. This is what lets a job branch instead of running steps in a fixed straight line.

code

java · 9 lines
java
@Bean
Job branchingJob(JobRepository repo, Step stepA, Step process, Step cleanup) {
    return new JobBuilder("branchingJob", repo)
        .start(stepA)
        .on("FAILED").to(cleanup)      // stepA ExitStatus == FAILED
        .from(stepA).on("*").to(process) // any other exit status
        .end()                          // close the flow
        .build();
}

go deeper

for a junior

Should know on().to().from() and that it branches on the previous step's outcome; a * catch-all is expected.

for a middle

Should articulate ExitStatus-string vs BatchStatus and the */? wildcard semantics.

for a senior

Should stress the coverage requirement and default ExitStatus==BatchStatus mapping plus when it diverges.

for a principal

Should frame conditional flow as the primitive under branching pipelines and know its interaction with restart and terminal transitions.

## The problem conditional flow solves A plain Spring Batch job runs steps in a fixed sequence: `start(a).next(b).next(c)`. Each `next` runs unconditionally as long as the prior step completed. **Conditional flow** lets the job *branch* — choose the next step based on the outcome of the previous one. ## ExitStatus: the thing patterns match against Every `StepExecution` ends with two status values: - **`BatchStatus`** — an enum (`COMPLETED`, `FAILED`, `STOPPED`, `ABANDONED`, ...) used internally by the framework to decide overall success and restartability. - **`ExitStatus`** — a *string-based* value (`ExitStatus.COMPLETED` has code `"COMPLETED"`, `ExitStatus.FAILED` has `"FAILED"`) that is **the value the flow-transition patterns are matched against**. This is the key point: `on(...)` matches the **ExitStatus code string**, not the BatchStatus enum. By default the ExitStatus code mirrors the BatchStatus (a completed step exits `"COMPLETED"`, a failed step exits `"FAILED"`), but you can override the ExitStatus to any custom string (e.g. `"COMPLETED WITH SKIPS"`, `"NO_DATA"`) to drive branching — see the follow-ups. ## The builder DSL ```java job.start(stepA) // first step .on("FAILED").to(cleanup) // if stepA exits FAILED -> cleanup .from(stepA).on("*").to(process) // otherwise -> process .end() // finish the flow definition .build(); ``` - **`.on(pattern)`** — registers a transition keyed by a pattern string. - **`.to(step)`** — the destination step when the pattern matches. - **`.from(step)`** — "go back and attach another transition off this already-named step." You need `from` because after the first `.to(...)` the builder's cursor has moved on; `from(stepA)` re-anchors it to stepA so you can add its *other* branch. - **`.end()`** — closes the flow builder (returns a `Job`-buildable). Note: bare `.end()` here terminates the flow definition; `.end()` used as `.on(x).end()` is a **terminal transition** (different meaning — see the terminal-transitions question). ## Wildcards Patterns support two wildcard characters (like simple file globs, NOT regex): - `*` — matches zero or more characters. `"C*"` matches `COMPLETED`; `"*"` matches anything (catch-all). - `?` — matches exactly one character. `"?OMPLETED"` matches `COMPLETED`. ## Coverage requirement If a step ends with an ExitStatus that matches **no** registered transition, the job throws (historically a message about the flow ending with no matching transition, exit status unhandled). Best practice: always add an `on("*")` catch-all so every outcome is handled. ## When to use Use conditional flow whenever downstream work depends on an upstream outcome: skip processing when a step finds no data, run a compensation/cleanup path on failure, choose between file formats, gate an expensive step behind a validation step, etc.

  • Why do you need `.from(step)` at all?
    After the first `.on(...).to(...)`, the builder cursor is positioned on the destination. `.from(stepA)` re-selects stepA so you can register a second (or third) transition off the same source step. Without it you would be adding transitions to the wrong step.
  • Does `on("FAILED")` match the BatchStatus or the ExitStatus?
    The ExitStatus code (a string). They usually coincide by default, but only the ExitStatus is what the pattern is compared against, which is why you can override the ExitStatus to drive custom branching.

saying these in an interview costs you the question

  • Thinking `on()` matches the BatchStatus enum rather than the ExitStatus string
  • Believing patterns are regular expressions (they are simple `*`/`?` globs)
  • Forgetting a catch-all and assuming unmatched statuses are silently ignored

context