Threadmill's Spring Boot integration is the lowest-boilerplate path. It still uses at-least-once delivery: handlers must be idempotent because recovery can run the same logical job more than once.
Spring Boot 4.x. This module requires Spring Boot 4.0 or newer. Spring Boot 3.x is not supported and will fail fast at application startup with a clear message.
Use Java 25 and add the Spring module plus one store. These examples use the 1.1.1 release.
implementation("com.hemju.threadmill:threadmill-spring-boot:1.1.1")
implementation("com.hemju.threadmill:threadmill-store-postgres:1.1.1")
// or: implementation("com.hemju.threadmill:threadmill-store-oracle:1.1.1") + your Oracle JDBC driver
// or: implementation("com.hemju.threadmill:threadmill-store-redis:1.1.1")The default Spring enqueue mode is after_commit: returned ids are reserved
before persistence, and the job insert can fail after the business transaction
commits. Observe AfterCommitEnqueueFailure, or choose join_transaction with
the same PostgreSQL or Oracle DataSource for atomic business/job writes. Cross-datastore
atomicity requires an application-owned durable outbox. See
transaction modes.
package com.example.mail;
import com.hemju.threadmill.core.handler.JobExecutionContext;
import com.hemju.threadmill.core.handler.JobHandler;
import com.hemju.threadmill.core.handler.JobPayload;
import com.hemju.threadmill.spring.Job;
import com.hemju.threadmill.spring.JobScheduler;
import org.springframework.stereotype.Component;
public record SendEmail(String to, String subject) implements JobPayload {}
@Component
@Job(queue = "email", timeout = "PT2M", maxAttempts = 5)
final class SendEmailHandler implements JobHandler<SendEmail> {
@Override
public void run(SendEmail payload, JobExecutionContext ctx) {
ctx.log("sending " + payload.subject() + " to " + payload.to());
// Make the external side effect idempotent.
}
}@RestController
final class MailController {
private final JobScheduler jobs;
MailController(JobScheduler jobs) {
this.jobs = jobs;
}
@PostMapping("/mail")
JobId send(@RequestBody SendEmail command) {
return jobs.enqueue(SendEmailHandler.class, command);
}
}JobScheduler verifies the handler/payload pair at enqueue time. If two
@Job beans handle the same payload type, startup fails and names both handlers.
By default the auto-configured JobScheduler is transaction-aware: a job
enqueued inside an active Spring transaction is held until afterCommit. A
rollback leaves nothing in the queue, so a job can never pick up state that
the surrounding transaction did not commit.
@Transactional
public void scheduleWelcome(UserCreated created) {
userRepo.save(created.toUser()); // pending write
jobs.enqueue(SendEmailHandler.class, new SendEmail(created.email(), "Welcome"));
// The job insert is attempted after commit; observe AfterCommitEnqueueFailure.
}The returned JobId is reserved synchronously (UUIDv7 is generated client
side), but the deferred store insert is attempted only after the transaction
commits. Within that transaction, store.findById(id) cannot yet find the job.
Outside a transaction the default wrapper inserts immediately. The
immediate mode also inserts immediately inside a transaction, but that job
can run before the business transaction commits and survives its rollback:
threadmill:
spring:
enqueue-mode: immediateFor Spring + Postgres, threadmill.spring.enqueue-mode=join_transaction makes
normal enqueue, scheduled enqueue, bulk enqueue, and dedup enqueue part of the
caller's SQL transaction.
Without a configured durable store or an application-provided JobStore,
startup fails. For disposable local development only, explicitly set
threadmill.store.memory.enabled=true; all jobs are lost when that process
stops. Configure PostgreSQL, Oracle, or Redis for durable work.
threadmill:
store:
redis:
mode: standalone
uri: redis://localhost:6379For Postgres, add threadmill-store-postgres and define a normal Spring
DataSource. Spring auto-configures PostgresJobStore from that DataSource
and runs pending Threadmill schema migrations by default. Use
threadmill.store.postgres.schema-mode=validate if your deployment pipeline
applies the DDL separately. See postgres-schema.md for
manual SQL and reset guidance.
For Oracle Database 19c+, add threadmill-store-oracle plus the Oracle JDBC
driver and define the DataSource the same way; Spring auto-configures
OracleJobStore and migrates by default (threadmill.store.oracle.schema-mode).
If both JDBC store modules are on the classpath, a jdbc:oracle: datasource
URL or threadmill.store.jdbc-type=oracle selects Oracle. See
oracle-schema.md for privileges and manual SQL.