What is the difference between FlowExecutionStatus (returned by a decider) and ExitStatus (produced by a step), and how do they relate to BatchStatus?
answer
- ExitStatus = step's String outcome
- FlowExecutionStatus = decider's String outcome
- BatchStatus = persisted enum lifecycle state
- flow .on() matches names with * and ? wildcards
- decider = programmatic ExitStatus at a junction
basics
~20 sA step produces an ExitStatus (a String outcome) that flow transitions match on. A decider returns a FlowExecutionStatus, a similar named status but produced by code instead of a step. Both are just names used to pick the next path; BatchStatus is the separate framework-level state (STARTED, COMPLETED, FAILED).
solid answer
~40 sExitStatus is the outcome a Step publishes when it finishes — a String name like COMPLETED or FAILED, optionally customized via a StepExecutionListener. Flow transitions (.on("X").to(...)) match against that name. FlowExecutionStatus is the analogous type a JobExecutionDecider returns programmatically; the flow builder matches .on(...) patterns against its name too, so a decider is a code-driven substitute for a step's ExitStatus at a branch point. Both are distinct from BatchStatus, an enum (STARTING, STARTED, COMPLETED, FAILED, STOPPED, ABANDONED, UNKNOWN) that records the actual persisted execution state in the repository. FlowExecutionStatus defines a few well-known names (COMPLETED, FAILED, STOPPED, UNKNOWN) but you can return any custom name. Confusing FlowExecutionStatus with ExitStatus in code won't compile; confusing either with BatchStatus is the common conceptual mistake.
code
java · 17 lines// A step can customize ExitStatus via a listener:
public class BranchingListener implements StepExecutionListener {
@Override
public ExitStatus afterStep(StepExecution stepExecution) {
boolean needsReview = stepExecution.getWriteCount() == 0;
return needsReview ? new ExitStatus("NO_DATA") : ExitStatus.COMPLETED;
}
}
// A decider does the same thing programmatically, returning FlowExecutionStatus:
public class ReviewDecider implements JobExecutionDecider {
@Override
public FlowExecutionStatus decide(JobExecution job, StepExecution step) {
boolean needsReview = step != null && step.getWriteCount() == 0;
return new FlowExecutionStatus(needsReview ? "NO_DATA" : "COMPLETED");
}
}go deeper
Know a step gives ExitStatus and a decider gives FlowExecutionStatus, both Strings you branch on.
Cleanly separate all three: ExitStatus, FlowExecutionStatus, BatchStatus, and explain .on() wildcard matching.
Explain how the flow engine aligns a step's ExitStatus into a FlowExecutionStatus and how severity ordering aggregates parallel flows.
Reason about status contracts as an API surface: stable custom names, catch-all fallbacks, and avoiding brittle string coupling across a large flow.
## Three status concepts, don't conflate them Spring Batch has three overlapping-looking 'status' types. Interviews probe whether you can keep them straight. ### 1. ExitStatus (`org.springframework.batch.core.ExitStatus`) - Produced by a **Step** (or Job) when it finishes. - Wraps an **exit code String** (`getExitCode()`) plus an optional description. - Default exit codes mirror the outcome: `COMPLETED`, `FAILED`, `NOOP`, `STOPPED`, `UNKNOWN`, `EXECUTING`. - You can override it — e.g. a `StepExecutionListener.afterStep()` returns a new `ExitStatus("CONTINUE")` to drive custom branching. - Flow transitions `.on(pattern)` match against the **exit code**. ### 2. FlowExecutionStatus (`org.springframework.batch.core.job.flow.FlowExecutionStatus`) - Returned by a **JobExecutionDecider.decide(...)**. - Wraps a **name String** (`getName()`). - Defines constants `COMPLETED`, `STOPPED`, `FAILED`, `UNKNOWN`; implements Comparable (severity ordering used internally to aggregate parallel flows). - The flow builder matches `.on(pattern)` against this name exactly like it does for a step's ExitStatus. So at a branch point, a decider's FlowExecutionStatus plays the same role an ExitStatus would. - You construct custom ones: `new FlowExecutionStatus("LARGE")`. ### 3. BatchStatus (`org.springframework.batch.core.BatchStatus`) - An **enum**: `STARTING, STARTED, STOPPING, STOPPED, FAILED, COMPLETED, ABANDONED, UNKNOWN`. - Represents the **actual, persisted lifecycle state** of a JobExecution/StepExecution in the JobRepository. - Ordered by severity; the framework uses it to decide overall success and restartability. - You generally read it, not match flow transitions on it. ## How ExitStatus and FlowExecutionStatus relate At the end of a step, Spring Batch derives the step's ExitStatus, and the flow engine wraps/aligns it with a FlowExecutionStatus to evaluate transitions. When you insert a decider, you're supplying that FlowExecutionStatus yourself instead of deriving it from a step. That's the whole point: **decider = programmatic ExitStatus at a junction**. ## Matching semantics `.on("COMPLETED")` is not an equals check — it's pattern matching with `*` (any chars) and `?` (single char). `.on("*")` is a catch-all. The most specific match wins; an unmatched status with no `*` fallback throws a flow exception (the job fails with 'next state not found'). ## Common gotchas - Returning `FlowExecutionStatus.COMPLETED` from a decider does NOT mean 'the job is done' — it's just a name to match; you still need a `.on("COMPLETED").to(...)` or `.end()`. - Custom names are case-sensitive strings; a typo silently sends you down the wrong (or no) branch. - Don't try to return an ExitStatus from a decider — different type, won't compile. - BatchStatus vs ExitStatus: BatchStatus is the enum truth of state; ExitStatus/FlowExecutionStatus are the Strings you route on.
- If a decider returns FlowExecutionStatus.COMPLETED, is the job finished?No. COMPLETED is just a name to match against .on("COMPLETED"). The flow still needs an explicit transition or .end() for that path; the persisted BatchStatus is what actually records job completion.
- How does .on("C*") behave?It's a wildcard pattern that matches any status name starting with C (e.g. COMPLETED, CONTINUE). .on() uses * (any sequence) and ? (single char); most-specific match wins, and .on("*") is the catch-all.
saying these in an interview costs you the question
- Saying a decider returns BatchStatus or ExitStatus.
- Believing .on() does exact String equality (it's wildcard pattern matching).
- Thinking returning COMPLETED from a decider ends the job by itself.
- Conflating BatchStatus (persisted enum) with the routing Strings.