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 target the unreleased 1.0.0 candidate; see the release status.
implementation("com.hemju.threadmill:threadmill-spring-boot:1.0.0")
implementation("com.hemju.threadmill:threadmill-store-postgres:1.0.0")
// or: implementation("com.hemju.threadmill:threadmill-store-redis:1.0.0")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 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 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.