Cloud-native batch

This section documents the current Spring Batch line — 6.0.x, on Spring Framework 7 and Spring Boot 4.1.x, with a Java 17+ baseline — as published at the Spring Batch reference documentation. No specific patch version is pinned. Some surfaces (Spring Cloud Task and Spring Cloud Data Flow orchestration, the deployer-based partition handler, and JSR-352) are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

A batch job in a container is still a batch job — but the environment changes some assumptions: the filesystem is ephemeral, the process may be evicted, configuration arrives from outside, and something else decides when to start it.

The twelve-factor lens

Factor What it means for a batch job

Config in the environment

Connection strings, input and output locations and tuning knobs come from environment variables or a config server — never from a properties file baked into the image. Business inputs stay JobParameters, so they remain part of the instance identity (Jobs, instances & parameters).

Processes are stateless

Local disk vanishes when the pod does. Stage files in object storage, and keep the only durable state in the JobRepository.

Backing services are attachable

The metadata database is a resource like any other; point at it by URL, and never assume it is the same instance as the business database.

Disposability

The pod can be evicted at any moment. Small chunks, restartable steps, and JobOperator.recover() on startup (Stopping, restart & recovery).

Logs as event streams

Write to stdout in a structured format; the platform collects it. Do not write a log file next to the output.

Admin processes

A batch job is an admin process — it should be launchable as a one-off run of the same image, with different parameters.

Externalized configuration

Spring Cloud Config supplies environment-specific settings, so one image runs in every environment:

spring:
  application:
    name: end-of-day-batch
  config:
    import: "optional:configserver:http://config-server:8888"
  batch:
    jdbc:
      initialize-schema: never    # the schema is managed by migrations, not the app
    job:
      enabled: true

Keep the distinction sharp: configuration (where the database is, how big a chunk is) belongs in properties; business inputs (which date, which file) belong in JobParameters, because they define which JobInstance this is.

Guarding remote calls

A step that calls a remote service inherits that service’s failure modes. Combine the framework’s retry (Fault tolerance: skip & retry) with a circuit breaker so a sustained outage fails fast instead of retrying a million times:

@Component
public class PricingItemProcessor implements ItemProcessor<Trade, PricedTrade> {

    private final CircuitBreaker circuitBreaker;
    private final PricingClient pricingClient;

    public PricingItemProcessor(CircuitBreakerFactory<?, ?> factory, PricingClient pricingClient) {
        this.circuitBreaker = factory.create("pricing");
        this.pricingClient = pricingClient;
    }

    @Override
    public PricedTrade process(Trade trade) {
        BigDecimal price = circuitBreaker.run(
                () -> pricingClient.price(trade.getIsin()),
                throwable -> trade.getLastKnownPrice());     // fallback: degrade, do not fail
        return new PricedTrade(trade, price);
    }
}

Whether the fallback is acceptable is a business decision — for a valuation run, a stale price may be worse than a failed job.

Spring Cloud Task

Spring Cloud Task is the companion project for short-lived applications. It records that a task ran — start time, end time, exit code, exit message — in its own TASK_EXECUTION tables, and links those records to the Spring Batch executions the task launched.

@SpringBootApplication
@EnableTask
public class EndOfDayTaskApplication {
    public static void main(String[] args) {
        SpringApplication.run(EndOfDayTaskApplication.class, args);
    }
}

The division of labour: Spring Batch knows about steps, chunks and restart; Spring Cloud Task knows about process lifecycle and exit codes, which is what an orchestrator actually schedules. A batch application that is launched as a container per run is naturally both.

This is linked, not documented in depth here — see Spring Cloud Task.

Spring Cloud Data Flow

Spring Cloud Data Flow orchestrates such tasks: registering an application, launching it with parameters, scheduling it, viewing execution history, and — through the composed task runner — running a DAG of tasks with conditional transitions, the same idea as a job flow but one level up, between applications.

# register the image, then launch it with job parameters
dataflow:> app register --name end-of-day --type task --uri docker://example/end-of-day-batch:1.4.0
dataflow:> task create end-of-day-task --definition "end-of-day"
dataflow:> task launch end-of-day-task --arguments "--spring.batch.job.name=endOfDayJob" \
             --properties "app.end-of-day.run.date=2026-01-31"

Also linked, not documented in depth — see Spring Cloud Data Flow documentation.

Kubernetes

A batch application maps directly onto a Kubernetes Job, and a scheduled one onto a CronJob — which is the platform playing the scheduler role Spring Batch deliberately does not (What Spring Batch is & running a first job):

apiVersion: batch/v1
kind: CronJob
metadata:
  name: end-of-day
spec:
  schedule: "30 2 * * 1-5"
  concurrencyPolicy: Forbid          # never two runs of the same job at once
  successfulJobsHistoryLimit: 7
  jobTemplate:
    spec:
      backoffLimit: 0                # let Spring Batch decide about retries, not the platform
      template:
        spec:
          restartPolicy: Never
          terminationGracePeriodSeconds: 120
          containers:
            - name: batch
              image: example/end-of-day-batch:1.4.0
              args:
                - "--spring.batch.job.name=endOfDayJob"
              env:
                - name: SPRING_DATASOURCE_URL
                  valueFrom:
                    secretKeyRef: { name: batch-db, key: url }

Four details matter in practice:

  • concurrencyPolicy: Forbid prevents an overrunning job from being launched twice — Spring Batch would refuse the duplicate instance anyway, but failing at the platform level is clearer;

  • backoffLimit: 0 stops Kubernetes from blindly restarting a failed pod; a restart should be a deliberate JobOperator.restart(…​) so that completed steps are skipped;

  • terminationGracePeriodSeconds must exceed one chunk, so a SIGTERM can stop the job cleanly rather than killing it mid-transaction;

  • the exit code should reflect the outcome (SpringApplication.exit(…​)), so the platform can distinguish success from failure.

The DeployerPartitionHandler (from Spring Cloud Task) extends partitioning to this world: instead of threads or long-running workers, each partition is launched as its own pod, which then exits. It suits bursty workloads on elastic infrastructure, and it is linked, not documented in depth here — the local and message-based partition handlers are covered in Scaling & parallel processing.

Further reading