skip to content

Why does Spring Batch model outcome as two separate concepts (a closed BatchStatus enum and an open ExitStatus code) instead of one?

level: principalimportance: nice to knowfreq 15%

answer

  1. Two audiences: framework (closed) vs app (open)
  2. Stable core / extensible edge; mechanism vs policy
  3. Merging forces lose-flexibility-or-lose-trust dilemma
  4. Default mapping makes complexity opt-in
  5. Restart trusts BatchStatus only

basics

~20 s

Because two different consumers need different vocabularies: the framework needs a small, fixed, trustworthy lifecycle enum for restart and monitoring, while application flows need an open, extensible outcome code for branching and reporting. One field can't safely serve both.

solid answer

~40 s

It's a separation-of-concerns decision. BatchStatus is a closed enum that the framework itself depends on — restartability, running-detection, aggregation via max() — so it must be a small, stable, machine-trusted vocabulary that application code cannot pollute. ExitStatus is an open value object whose String exitCode is meant to be extended by applications to express arbitrary outcomes (COMPLETED_WITH_SKIPS, NO_INPUT) and to drive flow branching. If you merged them, you'd face a dilemma: either freeze the vocabulary and lose branching flexibility, or let arbitrary strings into the field the framework trusts for restart, risking broken lifecycle logic. Keeping them separate lets the canonical lifecycle stay stable and safe while the outcome code varies freely, with a sensible default mapping so simple jobs never notice the distinction. It's the classic 'stable core, extensible edge' pattern.

go deeper

for a junior

Can state one is fixed and one is customizable.

for a middle

Explains framework-vs-application consumers at a basic level.

for a senior

Articulates the merge dilemma and the default-mapping-as-opt-in-complexity point.

for a principal

Generalizes to a reusable 'stable core / extensible edge' design principle and cites concrete failure modes the split prevents.

## The design tension A batch execution's 'result' has to satisfy **two distinct audiences**: 1. **The framework** — needs to decide restartability, detect running executions, aggregate step results into a job result, and persist a trustworthy record. This demands a **small, closed, well-defined vocabulary** with fixed semantics. 2. **The application / flow** — needs to express **domain-specific outcomes** and route the job accordingly ('completed but skipped rows', 'no input found', 'needs manual review'). This demands an **open, extensible** representation. A single field cannot serve both without compromise: - If it were a **closed enum**, applications couldn't add custom outcomes — no meaningful branching. - If it were an **open String**, the framework could no longer trust it for restart/lifecycle logic, because any typo or custom code could change machine behavior. ## The resolution: two types Spring Batch splits the concern: - **`BatchStatus`** (closed enum) — the **stable core**. The framework owns and trusts it. Its severity ordering (`max`, `isUnsuccessful`, `upgradeTo`) and restart rules are all defined on this closed set. - **`ExitStatus`** (open value object with String code) — the **extensible edge**. Applications shape it in listeners; flow transitions match on it. Its openness is a feature, not a bug. This is the well-known **'stable core / extensible edge'** or **'mechanism vs. policy'** pattern: keep the parts the system reasons about closed and invariant; make the parts users customize open. ## Default mapping keeps it approachable To avoid burdening simple jobs, ExitStatus is **derived from** BatchStatus by default (COMPLETED -> ExitStatus.COMPLETED, etc.). So a developer who never customizes anything sees the two agree and can ignore the distinction entirely — the complexity is **opt-in**. ## Consequences and trade-offs - **Safety**: custom exit codes can never accidentally make a COMPLETED job 'un-restartable' or vice versa — restart keys off BatchStatus only. - **Independence**: you can enrich reporting/branching without touching the lifecycle record. - **Cost**: two fields to understand; newcomers conflate them, and some expect returning `ExitStatus.FAILED` to fail a step (it doesn't — lifecycle is decided separately). - **Parallel severity systems**: BatchStatus has `max()`; ExitStatus has `and()`. Two mechanisms doing analogous things is a minor conceptual tax, justified by their different vocabularies. ## When the distinction earns its keep - Conditional flows that branch on nuanced outcomes. - Reporting/observability that wants richer codes than the enum offers. - Partitioned/remote steps aggregating heterogeneous outcomes. For a trivial linear job, the distinction is invisible — which is exactly the point of the default mapping. ## Architectural takeaway When you design status/result types in your own systems, ask *who consumes this and can they be trusted to extend it?* Split the closed, machine-critical vocabulary from the open, user-extensible one, and provide a default bridge so the simple case stays simple. Spring Batch's BatchStatus/ExitStatus split is a clean, real-world example of that principle.

  • Give a concrete failure mode that the separation prevents.
    If restart logic read the free-form exit code, an application customizing a completed step's code to something like 'DONE' could make the framework fail to recognize the instance as COMPLETED, or a typo could flip restart behavior. Keying restart off the closed BatchStatus enum makes that impossible.
  • How would you apply this principle in your own API design?
    Split a closed, machine-trusted status vocabulary (used for control-flow/persistence decisions) from an open, user-extensible outcome/label field (used for reporting/routing), and provide a default projection from the closed set to the open one so simple consumers ignore the distinction.

saying these in an interview costs you the question

  • Claiming the split is redundant / they should be one field.
  • Arguing ExitStatus should be an enum too (that would kill extensibility).
  • Saying restart should consider the custom exit code.

context