Tasklet steps

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.

Not every step is a bulk read/write loop. A tasklet step runs one arbitrary unit of work inside one transaction: move a file, truncate a staging table, call a stored procedure, invoke a service.

The Tasklet interface

Tasklet has a single method:

@FunctionalInterface
public interface Tasklet {
    RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception;
}

StepContribution is how the tasklet reports into the step’s counters (incrementReadCount, incrementWriteCount, setExitStatus); ChunkContext gives access to the StepExecution, the job parameters and both `ExecutionContext`s.

The return value drives the loop:

  • RepeatStatus.FINISHED — the work is done; the step completes.

  • RepeatStatus.CONTINUABLE — call me again. The framework commits the current transaction and re-invokes execute, which is how a tasklet processes a large job in restartable slices without loading everything at once.

public class PurgeExpiredTasklet implements Tasklet {

    private static final int BATCH = 5_000;

    private final PurgeService purgeService;

    public PurgeExpiredTasklet(PurgeService purgeService) {
        this.purgeService = purgeService;
    }

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
        int deleted = purgeService.deleteExpired(BATCH);   // deletes at most BATCH rows
        contribution.incrementWriteCount(deleted);
        // keep going until a pass deletes nothing -- each pass is its own transaction
        return deleted == 0 ? RepeatStatus.FINISHED : RepeatStatus.CONTINUABLE;
    }
}
@Bean
public Step purgeStep(JobRepository jobRepository, PlatformTransactionManager tx,
                      PurgeService purgeService) {
    return new StepBuilder("purgeStep", jobRepository)
            .tasklet(new PurgeExpiredTasklet(purgeService), tx)
            .build();
}

A lambda is fine for trivial work, as in What Spring Batch is & running a first job; a named class is better when the tasklet has dependencies or state.

TaskletStep and transaction semantics

TaskletStep is the implementation behind both tasklet steps and chunk steps — a chunk step is a TaskletStep whose tasklet is a ChunkOrientedTasklet. For a plain tasklet the semantics are simple:

  • one call to execute runs inside one transaction;

  • returning FINISHED commits it and ends the step;

  • returning CONTINUABLE commits it and starts a new one for the next call;

  • throwing commits nothing — the transaction rolls back and the step fails.

So a tasklet succeeds or fails as a whole. There is no partial credit, and no per-item skip semantics: the fault-tolerance options in Fault tolerance: skip & retry apply to chunk steps. A tasklet that must tolerate failure has to handle it itself, or the step must be made restartable.

Transaction attributes are configured the same way as for a chunk step:

@Bean
public Step ddlStep(JobRepository jobRepository, PlatformTransactionManager tx, DataSource dataSource) {
    DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
    attribute.setTimeout(600);   // DDL on a large table

    return new StepBuilder("ddlStep", jobRepository)
            .tasklet((contribution, chunkContext) -> {
                new JdbcTemplate(dataSource).execute("TRUNCATE TABLE STAGING_TRADE");
                return RepeatStatus.FINISHED;
            }, tx)
            .transactionAttribute(attribute)
            .build();
}

Note that some statements — DDL on many databases — commit implicitly and cannot be rolled back; the step must be written to be re-runnable rather than relying on the transaction. See SQL DDL.

Since 6.0, a long-running tasklet can also honour an external stop request by implementing StoppableTasklet (StoppableStep is the step-side contract — it extends Step, so a Tasklet cannot implement it) — see Stopping, restart & recovery.

The adapter tasklets

Three ready-made tasklets wrap code that already exists.

MethodInvokingTaskletAdapter

Calls a method on an existing bean — the shortest path from a service method to a step:

@Bean
public MethodInvokingTaskletAdapter reconcileTasklet(ReconciliationService reconciliationService) {
    MethodInvokingTaskletAdapter adapter = new MethodInvokingTaskletAdapter();
    adapter.setTargetObject(reconciliationService);
    adapter.setTargetMethod("reconcileDay");
    adapter.setArguments(new Object[] { LocalDate.now().minusDays(1) });
    return adapter;
}

@Bean
public Step reconcileStep(JobRepository jobRepository, PlatformTransactionManager tx,
                          MethodInvokingTaskletAdapter reconcileTasklet) {
    return new StepBuilder("reconcileStep", jobRepository)
            .tasklet(reconcileTasklet, tx)
            .build();
}

If the method returns an ExitStatus, it becomes the step’s exit status; any other return value is ignored. Combine it with @StepScope and late binding to pass a job parameter as the argument — see Step flow & listeners.

CallableTaskletAdapter

Runs a java.util.concurrent.Callable<RepeatStatus>, which is convenient when the work is already expressed that way or is produced by a factory:

@Bean
public CallableTaskletAdapter warmCacheTasklet(CacheWarmer cacheWarmer) {
    CallableTaskletAdapter adapter = new CallableTaskletAdapter();
    adapter.setCallable(() -> {
        cacheWarmer.warm();
        return RepeatStatus.FINISHED;
    });
    return adapter;
}

The Callable runs on the step’s own thread — the adapter does not make the work asynchronous by itself.

SystemCommandTasklet

Executes an operating-system command in a separate process, with a timeout and an interrupt check:

@Bean
@StepScope
public SystemCommandTasklet gzipTasklet(@Value("#{jobParameters['output.dir']}") String outputDir) {
    SystemCommandTasklet tasklet = new SystemCommandTasklet();
    tasklet.setCommand("gzip", "-9", outputDir + "/extract.csv");
    tasklet.setWorkingDirectory(outputDir);
    tasklet.setTimeout(60_000L);                       // milliseconds
    tasklet.setInterruptOnCancel(true);                // kill the process if the step is stopped
    tasklet.setTerminationCheckInterval(2_000L);
    tasklet.setSystemProcessExitCodeMapper(new SimpleSystemProcessExitCodeMapper());
    return tasklet;
}

Two cautions: the command runs outside the transaction, so its effect cannot be rolled back; and passing externally supplied values into a command line is an injection risk — validate them first.

When a tasklet beats a chunk step

Situation Why a tasklet

DDL or a set-based SQL statement (TRUNCATE, a bulk UPDATE, an index rebuild)

The database does the work in one statement; iterating rows in Java would be far slower.

Moving, renaming, unzipping or archiving a file

One filesystem operation, not a stream of items.

A single remote call — trigger an export, notify a downstream system

There is no collection to iterate; the step is one request.

Invoking a stored procedure that performs the whole batch

The procedure is the batch; the step just runs and audits it.

Preparing or validating before the real work

A cheap guard step that fails fast, often with allowStartIfComplete(true).

Conversely, anything that reads many items and writes many items belongs in a chunk step — it gets restartability at chunk granularity, skip/retry, bounded memory and the standard counters for free. See Chunk-oriented processing.

Further reading

For the full detail behind this page: