Building a Java EE batch workflow with JSR 352 in NetBeans
Batch processing sits at the heart of many Australian enterprise systems, from the end-of-day reconciliation jobs running inside Melbourne banks to the nightly statement generators at utilities in Brisbane. If you have been looking for a way to run those long-running tasks outside of a request-response cycle, the Java EE specification known as JSR 352 offers a surprisingly tidy answer. Combined with NetBeans IDE, the path from a blank project to a running batch job is shorter than most teams expect.
This walkthrough assumes you are comfortable with Java SE and have used NetBeans for basic web or enterprise development. You will create a simple batchlet, package it inside a WAR or EJB JAR, configure the job XML, and execute it through the job operator API. Along the way, the same patterns will let you build chunked jobs for reading and writing large record sets from sources like the ATO's bulk data feeds or internal telco billing streams.
The techniques below were tested on NetBeans 18 running on Windows 11 and Ubuntu, with Payara 6 as the application server. Everything should translate cleanly to other JSR 352 compliant runtimes, including WildFly and Open Liberty.
What JSR 352 actually gives you
JSR 352, the Java Batch specification, formalises two execution models inside the Jakarta EE platform. A batchlet is a single task-style step, ideal for triggers, clean-up routines, or calls to external systems. A chunk is a reader-processor-writer pipeline, perfect for processing large volumes of records in manageable pieces. NetBeans also recognises the META-INF/batch-jobs directory as the canonical location for job descriptors.
| Model | Best suited to | Typical duration | Failure handling |
|---|---|---|---|
| Batchlet | Triggers, system calls, single transactions | Seconds to minutes | Restart from beginning |
| Chunk | File imports, database migrations, billing runs | Minutes to hours | Restart from last checkpoint |
| Flow | Grouped steps that share checkpointing | Variable | Per-step policies |
Understanding the difference matters when you map a real business process. A chunked step in a Sydney-based superannuation fund might process millions of contribution rows, with checkpoints every fifty records. A batchlet in the same project might simply call a stored procedure to refresh a summary table. Mixing both within a single job gives you the best of each.
Preparing a batch project in NetBeans
Open NetBeans and choose File then New Project. Select Java with Maven and Web Application, giving the project a name such as MemberStatementJob. When the project is created, right-click the Other Sources node and add a new directory called batch-jobs under src/main/resources/META-INF. This is the place where every JSR 352 descriptor must live, and NetBeans will mark the folder accordingly once it sees a file with the batch.xml schema.
Add the JSR 352 API to your pom.xml if you are not running on a server that already provides it. Most modern servers such as Payara 6 and WildFly 27 ship the API out of the box, but having an explicit dependency keeps your code portable. The NetBeans Blog home page lists a few useful starter templates if you prefer a working baseline rather than scaffolding everything yourself.
If you are pulling legacy code into the project instead, the layout follows the same Maven structure, so swapping the source folder is straightforward. Make sure the descriptor ends up under META-INF/batch-jobs once the import is complete, otherwise the runtime will silently ignore it.
Writing the job XML descriptor
The job XML is the contract between your code and the runtime. A minimal descriptor for a single-batchlet job looks like this, and NetBeans will offer content completion against the batchXML_1_0.xsd schema:
<job id="member-statement-job" xmlns="https://jakarta.ee/xml/ns/jakartaee">
<step id="generate-step">
<batchlet ref="memberStatementBatchlet"/>
</step>
</job>
Save the file as member-statement-job.xml inside the META-INF/batch-jobs folder. The runtime uses the file name as the job name, so keep it descriptive. For chunked steps, you add <chunk> with item-count and checkpoint-policy attributes, plus reader, processor and writer class references. NetBeans flags missing references with a yellow underline, which catches most typos before you reach the deployment stage.
Implementing the batchlet
A batchlet is a plain CDI bean that implements jakarta.batch.api.Batchlet. The interface defines two methods: process for the main work, and stop for graceful shutdown when the operator calls for a halt. The implementation below writes a handful of statement lines to a JDBC datasource and returns COMPLETED once finished.
@Named
public class MemberStatementBatchlet implements Batchlet {
@Inject
private DataSource memberDs;
@Override
public String process() throws Exception {
try (Connection c = memberDs.getConnection();
PreparedStatement ps = c.prepareStatement(
"INSERT INTO statement_log(run_id, run_at) VALUES (?, ?)")) {
ps.setString(1, UUID.randomUUID().toString());
ps.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
ps.executeUpdate();
}
return BatchStatus.COMPLETED.name();
}
}
The stop method can stay empty for most jobs. If you need richer logging, the patterns described in the piece about a Python call duration decorator translate directly: wrap the process method with timing logs and capture entry and exit parameters. Australian teams often wire these logs into a central ELK stack hosted in a Sydney region, which keeps latency predictable for downstream alerts and aligns with the ap-southeast-2 footprint used by Atlassian and Canva.
Running the job with the job operator
The JobOperator interface is the runtime entry point. Inject it into a REST endpoint or a scheduled bean and call start, passing the job name and a properties map. The following snippet is enough to fire the job from a JSF page or a JAX-RS POST request:
@Inject
private JobOperator operator;
public long launch() {
Properties p = new Properties();
p.setProperty("runDate", LocalDate.now().toString());
return operator.start("member-statement-job", p);
}
NetBeans lets you run this directly against Payara through the Run action, and the server log shows the step transitions as they happen. Set a breakpoint inside process and use the IDE's debugger to step through; the runtime honours the JDWP attachment on a long-running batch, no dramas.
Common pitfalls and practical recommendations
A few recurring issues catch teams new to JSR 352. Skipping the META-INF/batch-jobs folder makes the runtime ignore the descriptor entirely, even if the XML is valid. Mixing batchlet and chunk semantics inside the same <step> confuses the parser and leads to vague JobStartException messages. Forgetting to commit a JDBC connection inside a chunk writer causes silent data loss when the job ends in FAILED state.
Practical habits that keep batch jobs healthy in production:
- Keep batchlet logic idempotent so a restart does not produce duplicate side effects.
- Store checkpoint metadata in the same database as the job data, ideally on a Sydney-region instance to match latency expectations.
- Set a
JobOperatorpolling interval in the application server, around five seconds, to balance responsiveness against log noise. - Use the
Metricstab in NetBeans to profile memory during the first real run of any chunked job. - For teams modernising older code, follow the NetBeans import guide before refactoring batch logic in place.
- Promote descriptors through the same Git workflow as application code and treat them as versioned artefacts rather than server config.
A working batch job on Payara or WildFly, with the descriptor validated by NetBeans and the batchlet exercised through the debugger, is a solid foundation. The same scaffolding scales to multi-step flows, partitioned chunks and decision-based routing, all within the IDE.