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-invokesexecute, 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
executeruns inside one transaction; -
returning
FINISHEDcommits it and ends the step; -
returning
CONTINUABLEcommits 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 ( |
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 |
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.