skip to content

Flow Control

Deciding what runs next: conditional transitions on exit status, programmatic deciders, and reusable flows or nested jobs. Interviewers use it when a job is a pipeline rather than a straight line.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

15

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

open as a page

What is a JobExecutionDecider in Spring Batch and why would you use one instead of relying on a step's ExitStatus?

level: juniorimportance: must knowfreq 55%

basics

~20 s

A JobExecutionDecider is a small class whose decide() method returns a status that tells the job which path to take next. You use it when the choice depends on custom logic, not just whether a step passed or failed.

open as a page

Explain the difference between BatchStatus and ExitStatus, and how you customize ExitStatus to drive a conditional transition.

level: middleimportance: must knowfreq 50%

basics

~20 s

BatchStatus is the framework's enum (COMPLETED/FAILED/STOPPED) deciding success and restart. ExitStatus is a customizable string that flow patterns (on(...)) match on. Override ExitStatus in a StepExecutionListener's afterStep to return custom codes like "NO_DATA" and branch on them.

open as a page

Show how you wire a JobExecutionDecider into a job's flow with .next(decider).on(...).to(...), including branching to multiple paths.

level: middleimportance: must knowfreq 50%

basics

~10 s

After a step, call .next(decider), then chain .on("NAME").to(step) for each possible status. Use .from(decider).on("OTHER").to(otherStep) to add more branches, and finish with .end().build().

open as a page

Compare FlowStep and JobStep: what is the key difference, and how do you choose between them?

level: middleimportance: must knowfreq 48%

basics

~20 s

FlowStep wraps a Flow and runs its steps inside the current job execution. JobStep wraps a whole Job and launches it as a separate execution with its own parameters and identity. Use FlowStep to reuse steps; use JobStep to nest an independent job.

open as a page

What do the terminal transitions `end()`, `fail()`, and `stop()` do at the end of an `on(...)` transition, and how do they differ for restart?

level: seniorimportance: must knowfreq 45%

basics

~20 s

They end the flow instead of going .to() another step. end() finishes the job as COMPLETED, fail() finishes as FAILED (restartable), and stop() halts as STOPPED (restartable, resumes from where it stopped). All three are attached after on(pattern).

open as a page

Explain JobStep: how do you nest a child Job inside a parent job, and how are the child's JobParameters supplied?

level: seniorimportance: must knowfreq 50%

basics

~20 s

JobStep is a Step that launches a whole child Job. You build it with StepBuilder.job(childJob), give it a JobLauncher, and a JobParametersExtractor that pulls the child's JobParameters from the parent step/job execution context. The child runs as its own separate JobExecution.

open as a page

What is a reusable Flow in Spring Batch, and how do you build one and plug it into a Job?

level: juniorimportance: should knowfreq 45%

basics

~10 s

A Flow is a named, reusable sequence of steps with transitions. You build one with FlowBuilder<Flow>().start(stepA).next(stepB).build(), then start a Job from it with JobBuilder.start(flow).end().build().

open as a page

What is the difference between FlowExecutionStatus (returned by a decider) and ExitStatus (produced by a step), and how do they relate to BatchStatus?

level: middleimportance: should knowfreq 45%

basics

~20 s

A 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).

open as a page

What is FlowStep, and when would you wrap a Flow as a single Step inside a job?

level: middleimportance: should knowfreq 40%

basics

~20 s

FlowStep turns a whole Flow into one Step. You build it with StepBuilder.flow(flow). The flow's inner steps run as normal within the same job execution, but the outer job sees them grouped as a single named step.

open as a page

When multiple `on(...)` patterns could match a step's ExitStatus (e.g. `"COMPLETED"` and `"C*"` and `"*"`), how does Spring Batch decide which transition wins?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Spring Batch picks the most specific matching pattern. A literal like "COMPLETED" beats "C*", which beats "*". Specificity is judged by how many wildcard characters the pattern has — fewer wildcards (and more literal characters) wins.

open as a page

How does a decider access data to make its decision, and what are the gotchas with the StepExecution argument and restart behavior?

level: seniorimportance: should knowfreq 35%

basics

~20 s

The decider reads from the JobExecution (job parameters, JobExecutionContext) and the preceding StepExecution (counts, its own step context). The StepExecution can be null if no step ran before the decider, so guard for it. Deciders should be deterministic so restarts route the same way.

open as a page

Design a Spring Batch job that validates a file, then branches: process normally, skip to a notify step when there is no data, run compensation and fail on validation error, and pause for manual review on an ambiguous result. Walk through the flow.

level: principalimportance: should knowfreq 22%

basics

~10 s

Have the validate step emit custom exit codes (VALID/NO_DATA/INVALID/AMBIGUOUS) via a StepExecutionListener or decider. Then branch: on("NO_DATA").to(notify).end(), on("INVALID").to(compensate) then compensate.on("*").fail(), on("AMBIGUOUS").stopAndRestart(review), on("*").to(process).end().

open as a page

In a production design, when is JobStep-based nested-job composition appropriate, and what identity/restart pitfalls must you guard against?

level: principalimportance: should knowfreq 30%

basics

~20 s

Use JobStep to orchestrate independently-runnable child jobs from a parent. Guard against duplicate identifying parameters (JobInstanceAlreadyCompleteException), reason about two separate restart boundaries, ensure the extractor forwards a unique run id, and confirm the launcher/isolation model fits.

open as a page

When would you choose a JobExecutionDecider versus alternatives (custom step ExitStatus, split/parallel flows, or externalizing orchestration), and what trade-offs guide that decision?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Use a decider when routing depends on logic a step's pass/fail can't express and you want the branch explicit in the job graph. Use step ExitStatus for simple success/failure branching, split flows for parallel independent paths, and external orchestration when the workflow spans many jobs or systems.

open as a page