skip to content

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%

answer

  1. end=COMPLETED, fail=FAILED, stop=STOPPED
  2. fail & stop are restartable, end is not
  3. stopAndRestart(step) picks resume point
  4. .end() overloaded: terminal vs close-flow
  5. declarative, not JobOperator.stop

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

solid answer

~40 s

After `on(pattern)` you can end the flow with a terminal transition instead of `.to(step)`: - **`end()`** — completes the job with BatchStatus COMPLETED. The job is considered finished successfully and normally can't be rerun with the same identifying parameters. Optional `end("CODE")` sets a custom exit code. - **`fail()`** — completes the job with BatchStatus FAILED. The job is restartable and on restart re-executes from the failed point. - **`stop()`** — halts with BatchStatus STOPPED; restartable, and `stop().and(restartStep)` (or `stopAndRestart`) lets you designate which step a restart resumes from. These let you model branch outcomes as explicit success/failure/pause endings — e.g. `on("FAILED").fail()` to mark the whole job failed when a branch fails, or `on("AMBIGUOUS").stop()` to pause for manual intervention. The distinction matters mainly for restart semantics and how the outcome is recorded.

code

java · 10 lines
java
@Bean
Job reviewJob(JobRepository repo, Step validate, Step process, Step review) {
    return new JobBuilder("reviewJob", repo)
        .start(validate)
        .on("INVALID").fail()                    // -> BatchStatus FAILED, restartable
        .from(validate).on("NEEDS_REVIEW").stopAndRestart(review) // -> STOPPED, resume at review
        .from(validate).on("*").to(process)
        .from(process).on("*").end()             // -> COMPLETED
        .build();
}

go deeper

for a junior

Likely only knows end() exists; distinguishing the three is a stretch.

for a middle

Should name all three and their BatchStatus outcomes.

for a senior

Expected to explain restart semantics and stopAndRestart resume point.

for a principal

Should design pause-for-approval and compensation flows and reason about ops/observability of fail() without exceptions.

## Terminal vs. non-terminal transitions A transition begins with `on(pattern)`. It then either **continues** the flow with `.to(step)` / `.stopAndRestart(step)` or **terminates** the flow with one of three terminal instructions: ### `end()` — successful completion Sets the job's BatchStatus to **COMPLETED**. The flow stops here and the job is a success. `end()` with no argument uses exit code "COMPLETED"; `end("MY_CODE")` sets a custom exit code on the job. Because the job is COMPLETED, restarting it with the *same identifying job parameters* is rejected (a completed job instance can't rerun) — this is the normal "done" outcome. ### `fail()` — force failure Sets BatchStatus to **FAILED**. Use it when a branch represents an error you want to surface as a failed job even though no exception was thrown (e.g., a validation step exits "INVALID" and you route `on("INVALID").fail()`). A FAILED job **is restartable**: rerunning the same job instance resumes and re-runs the parts that didn't complete. ### `stop()` — pause / halt Sets BatchStatus to **STOPPED**. The job halts cleanly (not an error). It is restartable. Two important forms: - `stop()` — plain stop; on restart the flow resumes normally. - `stopAndRestart(restartStep)` (builder method) / conceptually `stop().and(...)` — records that a subsequent restart should resume at `restartStep` rather than the default point. This models "pause here for a manual step or approval, then resume at a known place." ## Where they attach ```java job.start(validate) .on("INVALID").fail() // whole job FAILED .from(validate).on("NEEDS_REVIEW").stopAndRestart(review) // pause, resume at review .from(validate).on("*").to(process) // continue .from(process).on("*").end() // COMPLETED .build(); ``` ## `end()` overload nuance Inside a flow, `.end()` appears in two roles: 1. **`.on(x).end()`** — a *terminal transition* completing the job. 2. **`.end()` right after `.to(...)`/`.from(...)` chaining** — the `FlowBuilder`/`JobBuilder` call that *closes the flow definition* and returns something buildable. They are different methods with different receivers; context tells them apart. This overloading trips people up. ## Restart summary table | Terminal | BatchStatus | Restartable? | On restart | |---|---|---|---| | `end()` | COMPLETED | No (same params) | n/a | | `fail()` | FAILED | Yes | re-runs uncompleted parts | | `stop()` | STOPPED | Yes | resumes; `stopAndRestart` picks the resume step | ## When to use which - **`end()`**: a branch is a legitimate successful ending (e.g., "no data, nothing to do — done"). - **`fail()`**: a branch means the job genuinely failed and should be retried later. - **`stop()` / `stopAndRestart`**: you need a human or external trigger before continuing; pause without marking failure. ## Gotchas - Don't confuse `stop()` the transition with `JobOperator.stop(executionId)` (a runtime, out-of-band stop). The transition is declarative flow design. - `fail()` produces FAILED without an exception in logs, which can confuse ops — document why. - A job whose only completing branch is `fail()` will never be COMPLETED; make sure a success path exists.

  • After `on("X").end()`, can you rerun the job with the same job parameters?
    No. end() completes the job with BatchStatus COMPLETED, and Spring Batch refuses to re-run a completed job instance identified by the same identifying parameters. You'd need new identifying parameters (a new instance).
  • How does `stop()` as a transition differ from calling `JobOperator.stop(executionId)`?
    The transition is a declarative, design-time outcome baked into the flow that fires when a pattern matches. `JobOperator.stop` is an imperative, runtime request to stop a currently-running execution out-of-band. Both yield STOPPED, but one is part of the job graph and the other is operational.

saying these in an interview costs you the question

  • Saying a job ended with end() can be simply restarted with the same parameters
  • Claiming fail() is not restartable
  • Conflating the transition stop() with runtime JobOperator.stop()

context