Let's face it — cron jobs are fragile. They fail silently, don't retry intelligently, and vanish without a trace when your server crashes at 2 AM. For years, I relied on @Scheduled tasks in my Spring Boot apps, patching them with logging, retries, and monitoring scripts. But the deeper I went into production-scale scheduling, the more it felt like building a rocket out of duct tape.
That's when I discovered Temporal — a game-changing workflow engine that made my old cron setup feel like a relic.
In this article, I'll show you how I replaced every cron job in my Spring Boot app with durable, observable, fault-tolerant workflows using Temporal — and why I believe cron jobs are officially dead.
If you're tired of missed runs, manual retries, and sleepless nights watching your batch jobs like a hawk — this is the upgrade your backend needs.
The Problems With Cron Jobs in Spring Boot
Before we dive into the solution, let's look at the common issues I faced:
- Silent Failures: If a job throws an exception and no alerting is configured, it's easy to miss. Cron won't retry it.
- Lack of Resilience: Server restarted? Cron was supposed to run 5 minutes ago? Sorry, it's gone.
- Stateful Logic is a Nightmare: Chained job executions, timeouts, and conditional flows? Cron is stateless and primitive.
- Manual Retry Logic: Writing custom retry with backoff across jobs leads to boilerplate and bugs.
- Zero Observability: Logs are your only friend. There's no trace of how jobs executed over time unless you build your own dashboards.
I wanted time-based logic that felt native to my app, not duct-taped on.
Why Temporal?
Temporal offers:
- Durable Workflows: They survive restarts and are persisted.
- Retries and Timers: Built-in exponential backoff, retries, and wait mechanisms.
- Observability: Visual UI for monitoring workflow progress and state.
- Separation of Concerns: Business logic in workers, orchestration in workflows.
Most importantly, it's not just for background jobs — it's an architecture for long-running, fault-tolerant state machines.
Use Case: Replacing a Daily Batch Cron Job
Let's say you have a classic cron job:
@Scheduled(cron = "0 0 2 * * *")
public void generateDailyReport() {
reportService.generateReport();
}This runs every day at 2 AM. What if the app is down? What if reportService fails halfway?
Let's rebuild this using Temporal.
Setting Up Temporal in Spring Boot
1. Add Dependencies
<!-- Temporal SDK -->
<dependency>
<groupId>io.temporal</groupId>
<artifactId>temporal-sdk</artifactId>
<version>1.21.0</version>
</dependency>2. Define the Workflow Interface
@WorkflowInterface
public interface ReportWorkflow {
@WorkflowMethod
void generateDailyReport();
}3. Implement the Workflow
public class ReportWorkflowImpl implements ReportWorkflow {
private final ReportActivities activities =
Workflow.newActivityStub(ReportActivities.class,
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofMinutes(10))
.setRetryOptions(RetryOptions.newBuilder()
.setMaximumAttempts(5)
.build())
.build());
@Override
public void generateDailyReport() {
activities.generateReport();
// Schedule next run in 24 hours
Workflow.sleep(Duration.ofHours(24));
Workflow.continueAsNew(); // Recursively continue
}
}4. Define Activities (Business Logic)
@ActivityInterface
public interface ReportActivities {
void generateReport();
}
public class ReportActivitiesImpl implements ReportActivities {
@Override
public void generateReport() {
// Actual report generation logic
System.out.println("Report generated successfully.");
}
}5. Register and Start the Worker
@Configuration
public class TemporalConfig {
@Bean
public WorkflowServiceStubs service() {
return WorkflowServiceStubs.newInstance();
}
@Bean
public WorkflowClient client(WorkflowServiceStubs service) {
return WorkflowClient.newInstance(service);
}
@PostConstruct
public void startWorker() {
WorkerFactory factory = WorkerFactory.newInstance(client(service()));
Worker worker = factory.newWorker("report-task-queue");
worker.registerWorkflowImplementationTypes(ReportWorkflowImpl.class);
worker.registerActivitiesImplementations(new ReportActivitiesImpl());
factory.start();
}
}6. Trigger the First Run (Only Once)
@RestController
public class WorkflowTriggerController {
private final WorkflowClient client;
public WorkflowTriggerController(WorkflowClient client) {
this.client = client;
}
@GetMapping("/trigger-daily-report")
public String triggerReport() {
ReportWorkflow workflow = client.newWorkflowStub(
ReportWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskQueue("report-task-queue")
.setWorkflowId("daily-report-workflow")
.build()
);
WorkflowClient.start(workflow::generateDailyReport);
return "Workflow started.";
}
}Once started, this workflow will continue running forever — once every 24 hours — without the need for any external cron scheduler.
Key Benefits I Got
- Durability: No missed runs, even during downtime.
- Retry Logic: Handled automatically with exponential backoff.
- Separation of Concerns: My business logic was completely decoupled from orchestration.
- Visibility: Temporal Web UI showed every workflow run, result, and error.
- Scalability: I could shard long-running workflows easily across multiple workers.
Common Questions
Q: What happens if the app restarts? Temporal persists workflow state. The workers pick up from where they left off.
Q: Can I still run multiple types of jobs? Yes — each workflow can represent a different job with different timing logic.
Q: What about dynamic scheduling? Temporal workflows can accept parameters, so you can dynamically define when and how frequently tasks run.
Final Thoughts
Migrating to Temporal wasn't just a replacement — it was an upgrade in how I approached asynchronous, time-based logic in my Spring Boot app.
Cron jobs are like paper calendars — they work, until they don't. Temporal is like Google Calendar with reminders, alerts, backups, and team access.
If you're still relying on @Scheduled annotations and praying your server doesn't go down at 2 AM — it's time to evolve.
Kill your cron jobs. Forever.