"Task Execution & Scheduling" answered "when should this work start, and how do I run it without blocking a caller?" This lesson answers a different question entirely: once a piece of work is genuinely large — hundreds of thousands of records, not a handful — how do you structure, execute, track, and recover it? Spring Batch is Spring's answer, and it's built on top of neither @Async nor @Scheduled; it's a separate concern that often gets triggered BY one of them.
The Real Problem: A Nightly Batch of 500,000 Records
Picture a real requirement: every night at 2 AM, the application must process 500,000 customer records — read them from the database, validate and transform each one, write the results somewhere else, survive a failure partway through without corrupting anything, and be able to restart from where it failed instead of reprocessing all 500,000 records from scratch.
The naive approach looks obvious at first:
@Scheduled(cron = "0 0 2 * * *")
public void processCustomers() {
// read 500,000 records
// process them
// write them
}
This compiles, and for a few hundred rows it might even work. At real scale it falls apart: loading 500,000 records into memory at once is wasteful (or impossible); a single giant transaction around all of it either locks resources for far too long or, if it's not transactional at all, leaves the database in a half-written state on any failure; and if the process crashes at record 300,001, there is no record anywhere of how far it actually got — a restart means starting over from record 1.
@Scheduled and Spring Batch are not solving the same problem — don't reach for one expecting the other's guarantees.
@Scheduled
→ "WHEN should this work start?"
Spring Batch
→ "HOW should a large batch operation be structured,
executed, tracked, and restarted?"
The two are not competitors — they're often used together, each doing only the part it's actually good at:
@Scheduled
|
v
launch Spring Batch Job
|
v
Spring Batch manages the actual batch processing
A Mental Model: Job, Step, and Chunk-Oriented Processing
Before any Spring Batch class or annotation, here is the shape everything else in this lesson fills in:
Job
|
+-- Step 1
| |
| +-- read
| +-- process
| +-- write
|
+-- Step 2
|
+-- read
+-- process
+-- write
A Job is the overall batch process — "import tonight's orders," end to end. A Step is one phase of that process — a Job with several genuinely distinct phases (import, then reconcile, then report) would have several Steps, run in sequence. A Step is commonly chunk-oriented: it reads a batch of items, optionally transforms each one, and writes them — and inside a chunk-oriented step, three roles do the actual work: an ItemReader reads items one at a time, an ItemProcessor optionally transforms or validates each one, and an ItemWriter writes a whole group of them at once. Spring Batch itself manages the loop that drives these three — you write what each one does, not the loop that calls them.
The Running Example: Importing Orders from a CSV File
Every piece of this lesson builds toward ONE concrete scenario, reused throughout rather than a new toy example per section: importing customer orders from a CSV file into a database.
orderId,customerId,amount
1001,C001,125.50
1002,C002,89.90
1003,C003,250.00
The job reads each row from orders.csv, converts it into an Order, validates or transforms it, and writes valid orders to the database — exactly the reader/processor/writer roles from the mental model above, applied to a real pipeline:
orders.csv
|
v
ItemReader
|
v
Order
|
v
ItemProcessor
|
v
Processed Order
|
v
ItemWriter
|
v
Database
Chunk Processing: Reading, Writing, and Committing in Groups
.chunk(100) is easy to read as "process 100 items" and move on — but what actually happens underneath is the single most important mechanic in this lesson.
Read item 1
Read item 2
Read item 3
...
Read item 100
|
v
Process items
|
v
Write items 1-100
|
v
COMMIT TRANSACTION
Then the exact same cycle repeats, chunk after chunk:
101-200
|
v
write
|
v
COMMIT
201-300
|
v
write
|
v
COMMIT
Chunking matters for several concrete reasons at once: memory usage stays bounded (100 Order objects in memory, not 500,000); each chunk is its own transaction boundary, so a failure doesn't leave a half-written mess spanning the entire job; and it's what makes restart meaningful at all — without chunk boundaries, there would be no smaller unit than "the whole job" to know the status of.
Items 1-100 → committed
Items 101-200 → committed
Items 201-300 → FAILURE
Conceptually, the first 200 items are safely committed, and only the third chunk failed — Spring Batch's execution metadata (covered shortly) is what lets the framework know processing reached chunk 3, not chunk 1. Exactly which items get reprocessed on a restart depends on the job's configuration and the state that was persisted — a detail this lesson comes back to in "Restartability and the JobRepository," rather than promised here in the abstract.
A Minimal, Complete Job Configuration
With the mental model in place, here is a genuinely complete (if minimal) Job and Step for the order-import example.
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
record Order(String orderId, String customerId, java.math.BigDecimal amount) {
}
// The two building blocks every Spring Batch job needs: a Job (the whole
// process) built from one or more Steps (a phase of that process). Both
// builders require a JobRepository -- Spring Batch's own store for
// execution state, covered later in this lesson -- because every Job and
// Step it creates is TRACKED there from the moment it runs, not just
// executed and forgotten.
@Configuration
class OrderImportJobConfig {
@Bean
public Job orderImportJob(JobRepository jobRepository, Step importStep) {
return new JobBuilder("orderImportJob", jobRepository)
.start(importStep)
.build();
}
// chunk(100, transactionManager) is what makes this step CHUNK-ORIENTED:
// reader/processor/writer are wired together here, and Spring Batch
// itself drives the read-process-write loop between them in groups of
// 100 -- see "Chunk Processing" for exactly what that means.
@Bean
public Step importStep(JobRepository jobRepository,
PlatformTransactionManager transactionManager,
ItemReader<Order> orderItemReader,
ItemProcessor<Order, Order> orderItemProcessor,
ItemWriter<Order> orderItemWriter) {
return new StepBuilder("importStep", jobRepository)
.<Order, Order>chunk(100, transactionManager)
.reader(orderItemReader)
.processor(orderItemProcessor)
.writer(orderItemWriter)
.build();
}
}
Read it top to bottom the way Spring Batch itself executes it:
Job
↓
start(importStep)
↓
Step
↓
chunk(100)
↓
reader → processor → writer
orderImportJob(...) is the Job — here, a single Step (importStep) is its entire process; a JobBuilder needs a JobRepository because, as covered further below, every Job Spring Batch creates is tracked there. importStep(...) is where the real work is described: .chunk(100, transactionManager) declares chunk-oriented processing with a chunk size of 100, and .reader(...)/.processor(...)/.writer(...) plug in the three roles from the mental model — each supplied as its own bean, detailed next.
ItemReader, ItemProcessor, and ItemWriter for the Order Import
Each of the three roles referenced above needs a concrete implementation for THIS example — reading CSV rows, filtering invalid amounts, and writing to the database.
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import javax.sql.DataSource;
import java.math.BigDecimal;
record Order(String orderId, String customerId, BigDecimal amount) {
}
// The three pieces a chunk-oriented step actually drives: read one Order
// at a time from orders.csv, optionally transform/validate it, then write
// it. Spring Batch calls these in a loop -- reader.read() repeatedly until
// the chunk size is reached, processor.process() on each item, then a
// SINGLE writer.write(...) call for the whole chunk -- see "Chunk
// Processing" for why that grouping matters.
@Configuration
class OrderImportComponents {
@Bean
public FlatFileItemReader<Order> orderItemReader() {
return new FlatFileItemReaderBuilder<Order>()
.name("orderItemReader")
.resource(new ClassPathResource("orders.csv"))
.linesToSkip(1) // skip the CSV header row (orderId,customerId,amount)
.delimited()
.names("orderId", "customerId", "amount")
.fieldSetMapper(fieldSet -> new Order(
fieldSet.readString("orderId"),
fieldSet.readString("customerId"),
fieldSet.readBigDecimal("amount")))
.build();
}
// Returning null here is a deliberate Spring Batch convention: it
// means "filter this item out." A filtered item is silently dropped
// and never reaches the writer -- this is NOT an error and does not
// fail the step, unlike the validation failures covered later in
// "Fault Tolerance: Skip and Retry."
@Bean
public ItemProcessor<Order, Order> orderItemProcessor() {
return order -> {
if (order.amount().signum() < 0) {
return null; // filtered out: a negative amount is not written, not an error
}
return order;
};
}
@Bean
public JdbcBatchItemWriter<Order> orderItemWriter(DataSource dataSource) {
return new JdbcBatchItemWriterBuilder<Order>()
.dataSource(dataSource)
.sql("INSERT INTO orders (order_id, customer_id, amount) VALUES (:orderId, :customerId, :amount)")
.beanMapped()
.build();
}
}
orderItemReader() builds a FlatFileItemReader<Order> that reads orders.csv, skips its header row, splits each line on the delimiter, and maps the three columns onto an Order. orderItemProcessor() shows an important, easy-to-miss Spring Batch behavior: returning null from an ItemProcessor filters the item out — it is silently dropped and never reaches the writer, which is exactly how a negative-amount order gets excluded here without failing anything. orderItemWriter() writes each chunk's Orders to the database with a single batched SQL insert per chunk, rather than one insert per row.
Filtering (returning null) and failing (throwing an exception) are different outcomes with different consequences — filtering quietly excludes one item and the step continues normally; throwing signals a real problem, and by default fails the whole step. "Fault Tolerance: Skip and Retry," later in this lesson, covers how to make certain exceptions recoverable instead.
JobParameters, JobInstance, and JobExecution
Running the SAME logical job again isn't automatically the same thing as running it for the first time — Spring Batch needs a way to tell these apart, and that's what these three concepts are for.
Job: orderImportJob
JobParameters:
file=orders.csv
businessDate=2026-08-24
A Job combined with a specific set of JobParameters identifies a JobInstance:
Job
+
JobParameters
|
v
JobInstance
This matters because a batch job is usually about a specific input, not just "run the logic again." Two different businessDate values genuinely represent two different pieces of work:
businessDate=2026-08-24
→ one logical job instance
businessDate=2026-08-25
→ another logical job instance
But even a single JobInstance can be ATTEMPTED more than once — if it fails and is restarted, that's still the same logical instance, just a new attempt at it. That attempt is a JobExecution:
Job
|
+-- JobInstance (businessDate=2026-08-24)
|
+-- JobExecution #1 → FAILED
|
+-- JobExecution #2 → COMPLETED
A StepExecution is the same idea one level down — a single Step's own attempt, within a particular JobExecution. Spring Batch stores all of this (which you'll see next) precisely so a failed JobExecution can be told apart from a successful one for the exact same JobInstance, instead of every run looking identical.
Restartability and the JobRepository
The reason all of the bookkeeping above exists becomes concrete with a failure scenario.
500,000 records
1-100,000 ✓
100,001-200,000 ✓
200,001-300,000 ✓
300,001-400,000 ✗ application crashes
A serious batch framework shouldn't have to treat this as "the whole operation failed, start over" — it should be able to persist enough execution state to support restarting the failed attempt, picking up from roughly where it left off, rather than blindly reprocessing the first 300,000 records that already succeeded. This is exactly what the JobRepository is for:
Your batch job
|
v
JobRepository
|
+-- job execution status
+-- step execution status
+-- execution metadata
+-- restart-related state
This is what separates Spring Batch from simply writing:
while (...) {
read();
process();
write();
}
A plain loop like that has no memory of its own progress — if it dies at record 300,001, nothing anywhere records that fact. The JobRepository is where JobExecutions and StepExecutions actually get persisted, along with an ExecutionContext — a small bag of state a Step can use to remember details relevant to resuming it. This lesson introduces ExecutionContext only far enough to explain why restart is possible at all; exactly which items get skipped versus reprocessed on a given restart depends on the step's own configuration, not on some universal guarantee.
What Happens When a Step Fails
Put concretely, a chunk-oriented step's failure path looks like this:
Reader
↓
Processor
↓
Writer
↓
Database failure
A few things are true regardless of exactly where in a chunk something goes wrong: the chunk is the transaction boundary, so a failure inside it rolls back that entire chunk's writes, not just the one item that triggered it; the JobExecution/StepExecution metadata records that this attempt failed and roughly how far it got; and whether — and how precisely — a restart resumes from that point depends on the step's configuration (chunk size, whether it's restartable, what state it stores), not on an automatic, universal guarantee that every failure resumes from the exact failed item.
Fault Tolerance: Skip and Retry
Only after the core model — Job, Step, reader/processor/writer, chunks, the repository — makes sense does it make sense to introduce fault tolerance for individual bad items.
Picture 10,000 records where record #532 has malformed data. Without fault tolerance, that one bad record fails the ENTIRE step. With it configured:
import org.springframework.batch.core.Step;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.TransientDataAccessException;
import org.springframework.transaction.PlatformTransactionManager;
record Order(String orderId, String customerId, java.math.BigDecimal amount) {
}
class InvalidOrderException extends RuntimeException {
InvalidOrderException(String message) {
super(message);
}
}
// The SAME importStep from "A Minimal, Complete Job Configuration", with
// fault tolerance added -- faultTolerant() switches on Spring Batch's
// skip/retry machinery for this step, which is OFF by default.
@Configuration
class FaultTolerantImportStepConfig {
@Bean
public Step importStep(JobRepository jobRepository,
PlatformTransactionManager transactionManager,
ItemReader<Order> orderItemReader,
ItemProcessor<Order, Order> orderItemProcessor,
ItemWriter<Order> orderItemWriter) {
return new StepBuilder("importStep", jobRepository)
.<Order, Order>chunk(100, transactionManager)
.reader(orderItemReader)
.processor(orderItemProcessor)
.writer(orderItemWriter)
.faultTolerant()
// A genuinely malformed row (a missing customerId, say)
// throws InvalidOrderException instead of being silently
// filtered the way a negative amount is -- skip() lets the
// STEP continue past it instead of failing the entire job,
// up to 10 such rows.
.skip(InvalidOrderException.class)
.skipLimit(10)
// A database hiccup is often TEMPORARY -- retry() tries the
// same write again (up to 3 times) before giving up,
// instead of treating a transient failure as permanent.
.retry(TransientDataAccessException.class)
.retryLimit(3)
.build();
}
}
.faultTolerant() switches this behavior on for the step — it's off by default. .skip(InvalidOrderException.class).skipLimit(10) means: if this specific kind of error occurs, don't fail the whole job immediately — skip that one item and continue, but only up to 10 skips total; the 11th failure of that type still fails the step. .retry(TransientDataAccessException.class).retryLimit(3) means something different: this kind of error may be temporary (a brief database hiccup), so retry the SAME operation up to 3 times before giving up on it — skip is about tolerating a genuinely bad item, retry is about tolerating a possibly-temporary failure of an otherwise-good one.
Combining @Scheduled with Spring Batch
This lesson follows "Task Execution & Scheduling" directly, so it's worth connecting the two mechanisms explicitly.
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
// @Scheduled decides WHEN this runs; everything after that line is Spring
// Batch deciding HOW the actual import is executed, tracked, and (if
// needed) restarted -- "Task Execution & Scheduling" and this lesson
// working together, each doing only the job it's actually responsible for.
@Component
class NightlyOrderImportScheduler {
private final JobLauncher jobLauncher;
private final Job orderImportJob;
NightlyOrderImportScheduler(JobLauncher jobLauncher, Job orderImportJob) {
this.jobLauncher = jobLauncher;
this.orderImportJob = orderImportJob;
}
@Scheduled(cron = "0 0 2 * * *")
public void launchNightlyImport() throws Exception {
// JobParameters identify THIS run -- "file" says which CSV to
// read, "businessDate" says which day's import this is. Together
// with the Job itself, these parameters are what Spring Batch uses
// to tell one logical run apart from another -- see "JobParameters,
// JobInstance, and JobExecution".
JobParameters jobParameters = new JobParametersBuilder()
.addString("file", "orders.csv")
.addLocalDate("businessDate", LocalDate.now())
.toJobParameters();
jobLauncher.run(orderImportJob, jobParameters);
}
}
@Scheduled(cron = "0 0 2 * * *") decides WHEN launchNightlyImport() runs — nothing new from the previous lesson. What's new is what it does: it builds JobParameters (the file/businessDate pair from earlier) and hands them, along with the Job itself, to a JobLauncher — the object that actually starts a JobInstance/JobExecution running.
2:00 AM
|
v
@Scheduled
|
v
JobLauncher
|
v
Spring Batch Job
|
v
Step
|
v
Reader → Processor → Writer
The distinction to hold onto: @Scheduled decides when to start; Spring Batch decides how to execute and manage the batch process once it does. This is one of the two central takeaways of this lesson.
@Async vs. @Scheduled vs. Spring Batch
With all three mechanisms from this category now covered, it's worth stating precisely what each one is actually for:
@Async
→ Run this method in the background.
@Scheduled
→ Start this method according to a schedule.
Spring Batch
→ Execute and manage a structured batch job,
including steps, chunks, transactions,
execution metadata, failure handling and restartability.
These aren't competing choices for the same problem — they coexist, and often do, exactly as ScheduledOrderImportLauncher shows: @Scheduled triggers a launch, which is itself neither @Async nor a batch job by itself, but starts one.
Putting It All Together: The Nightly Order Import, End to End
Nightly order import
@Scheduled
|
v
JobLauncher
|
v
Order Import Job
|
v
Import Step
|
+--> ItemReader
|
+--> ItemProcessor
|
+--> ItemWriter
|
v
Chunk transaction
|
v
JobRepository
In plain language: the scheduler triggers the job at 2 AM; Spring Batch creates or identifies the JobExecution for this JobInstance; the step starts; the reader reads Orders from the CSV; the processor validates and transforms each one (filtering some, as covered earlier); the writer writes the survivors to the database; all of this happens in chunks of 100; each chunk's writes commit together as one transaction; execution metadata is recorded in the JobRepository throughout; and if the job fails partway through, that stored execution state is what a restart can use to avoid starting completely over.
What Spring Batch Is NOT
A few explicit non-goals, to keep it from blurring together with the previous lesson: Spring Batch is not simply another scheduler — it has nothing to say about WHEN to run (that's @Scheduled's job, or a manual trigger, or another scheduler entirely). It is not simply a thread pool — chunking and transactions are its core concern, not concurrent execution. It is not simply a while loop around database records — the JobRepository's tracked execution state is what a plain loop never has. And it is not automatically a parallel-processing framework — a standard chunk-oriented step, exactly like the one built in this lesson, runs sequentially; genuine parallelism (partitioning, parallel steps) is a distinct, more advanced topic this lesson only names, not teaches.
Best Practices
- Reach for Spring Batch when the real requirement is structured, trackable, restartable processing of a large dataset — not for every scheduled task, which
@Scheduledalone often already covers. - Choose a chunk size deliberately: large enough to be efficient, small enough that a rollback or a restart doesn't waste excessive work.
- Use
ItemProcessorreturningnullfor legitimate filtering, and reserve thrown exceptions for genuine failuresskip/retryshould react to. - Keep
skipLimit/retryLimitvalues deliberately small and specific to the exception types that are genuinely tolerable — a bare.skip(Exception.class)hides real bugs behind a high skip count. - Let
@Scheduleddecide when a job launches and let Spring Batch decide how it runs — don't blur the two together in one method.
Common Mistakes
- Assuming
@Scheduleditself provides any of Spring Batch's tracking, chunking, or restart behavior — it only decides when something starts. - Confusing a
Job(the reusable definition) with aJobExecution(one attempt at running it) — the sameJobbean produces manyJobExecutions over time. - Confusing a
JobInstance(aJobplus its identifyingJobParameters) with aJobExecution(one attempt at that specific instance) — oneJobInstancecan have severalJobExecutions if it's retried. - Not accounting for chunk boundaries — assuming every item is committed independently, when in fact a whole chunk commits (or rolls back) together.
- Assuming every failure automatically resumes from the exact failed item in every configuration, rather than depending on the step's own restart configuration.
- Reaching for partitioning or parallel steps before a plain, sequential chunk-oriented step is well understood.
- Creating several unrelated toy jobs to demonstrate different APIs instead of building depth in one coherent example, the way this lesson stuck to a single order-import job throughout.
- Writing the full Job/Step configuration before explaining the execution model it implements — the code means little without the mental model from earlier in this lesson.
Summary, Cheat Sheet, and Glossary
Summary
For every major abstraction in this lesson, the question it answers:
Job
→ What is the overall unit of work?
Step
→ How do I divide the job into meaningful phases?
ItemReader
→ How do I read one item at a time?
ItemProcessor
→ Where do I transform/validate an item?
ItemWriter
→ Where do I persist/output processed items?
chunk(...)
→ How do I group processing into transaction boundaries?
JobRepository
→ Where does Spring Batch keep execution state?
JobLauncher
→ How do I start a job?
JobParameters
→ What identifies a particular run/input?
JobExecution
→ What happened during an execution attempt?
- Spring Batch structures, executes, tracks, and can restart large batch operations — a genuinely different concern from
@Scheduled's "when." - A
Jobis built from one or moreSteps; a chunk-orientedStepdrives anItemReader→ItemProcessor→ItemWriterpipeline in groups (a "chunk"), each chunk committing as one transaction. - A
Jobplus itsJobParametersidentifies aJobInstance; each attempt at running that instance is aJobExecution, tracked (along with eachStepExecution) in theJobRepository. - Fault tolerance (
skip/retry) is opt-in via.faultTolerant(), and distinguishes genuinely bad items (skip) from possibly-temporary failures (retry). @Scheduledand Spring Batch commonly work together: the scheduler decides when to launch a job, Spring Batch decides how that job actually runs.
Cheat Sheet
// Job + chunk-oriented Step
@Bean
Job orderImportJob(JobRepository repo, Step importStep) {
return new JobBuilder("orderImportJob", repo).start(importStep).build();
}
@Bean
Step importStep(JobRepository repo, PlatformTransactionManager tx,
ItemReader<Order> reader, ItemProcessor<Order, Order> processor,
ItemWriter<Order> writer) {
return new StepBuilder("importStep", repo)
.<Order, Order>chunk(100, tx)
.reader(reader).processor(processor).writer(writer)
.build();
}
// ItemProcessor: return null to filter, throw to fail/skip
ItemProcessor<Order, Order> processor() {
return order -> order.amount().signum() < 0 ? null : order;
}
// Fault tolerance
.faultTolerant()
.skip(InvalidOrderException.class).skipLimit(10)
.retry(TransientDataAccessException.class).retryLimit(3)
// JobParameters identify a JobInstance
JobParameters params = new JobParametersBuilder()
.addString("file", "orders.csv")
.addLocalDate("businessDate", LocalDate.now())
.toJobParameters();
// @Scheduled launches the job; Spring Batch runs it
@Scheduled(cron = "0 0 2 * * *")
void launch() throws Exception {
jobLauncher.run(orderImportJob, params);
}
Glossary
- Job: the overall, reusable definition of a batch process, made of one or more Steps.
- Step: one phase of a Job, commonly chunk-oriented (read, process, write).
- Chunk: a group of items processed and committed together as one transaction.
- ItemReader / ItemProcessor / ItemWriter: the three roles a chunk-oriented step drives — read one item, optionally transform/validate it, write a whole chunk at once.
- JobParameters: the identifying input (like a file name or a business date) that, combined with a Job, defines a JobInstance.
- JobInstance: a specific Job run identified by its JobParameters — the same JobInstance can be attempted more than once.
- JobExecution / StepExecution: one attempt at running a JobInstance / a Step within it, tracked with a status (e.g. FAILED, COMPLETED).
- JobRepository: where Spring Batch persists execution state — job/step status, metadata, and restart-related information.
- JobLauncher: the component that actually starts a Job running with a given set of JobParameters.
- Skip / Retry: fault-tolerance options — skip tolerates a genuinely bad item (up to a limit); retry tolerates a possibly-temporary failure of an otherwise-good operation.