java
60 lines · 10 steps
Retrying failed jobs on a Spring schedule
A scheduled Spring component pulls failed jobs in batches and retries them with exponential backoff until they succeed or die.
Explained by
highlit
1@Component
2@RequiredArgsConstructor
3@Slf4j
4public class FailedJobReprocessor {
5
6 private static final int MAX_ATTEMPTS = 5;
7 private static final int BATCH_SIZE = 50;
8
9 private final ScheduledJobRepository jobRepository;
10 private final JobDispatcher jobDispatcher;
11
12 @Scheduled(fixedDelayString = "${jobs.reprocess.interval-ms:30000}")
13 @Transactional
14 public void reprocessFailedJobs() {
15 Instant now = Instant.now();
16 List<ScheduledJob> due = jobRepository.findRetryable(
17 JobStatus.FAILED, now, MAX_ATTEMPTS,
18 PageRequest.of(0, BATCH_SIZE));
19
20 if (due.isEmpty()) {
21 return;
22 }
23
24 log.info("Reprocessing {} failed jobs", due.size());
25
26 for (ScheduledJob job : due) {
27 job.setStatus(JobStatus.RUNNING);
28 job.setAttempts(job.getAttempts() + 1);
29 job.setLastAttemptAt(now);
30
31 try {
32 jobDispatcher.dispatch(job.getType(), job.getPayload());
33 job.setStatus(JobStatus.COMPLETED);
34 job.setLastError(null);
35 } catch (Exception ex) {
36 log.warn("Job {} failed on attempt {}", job.getId(), job.getAttempts(), ex);
37 job.setLastError(truncate(ex.getMessage()));
38 if (job.getAttempts() >= MAX_ATTEMPTS) {
39 job.setStatus(JobStatus.DEAD);
40 } else {
41 job.setStatus(JobStatus.FAILED);
42 job.setNextRetryAt(now.plus(backoff(job.getAttempts())));
43 }
44 }
45
46 jobRepository.save(job);
47 }
48 }
49
50 private Duration backoff(int attempts) {
51 return Duration.ofSeconds((long) Math.pow(2, attempts) * 10);
52 }
53
54 private String truncate(String message) {
55 if (message == null) {
56 return "unknown error";
57 }
58 return message.length() > 500 ? message.substring(0, 500) : message;
59 }
60}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Capping attempts and moving exhausted jobs to a DEAD state prevents infinite retry loops.
- 2Exponential backoff spreads retries out so a flaky dependency isn't hammered on every cycle.
- 3Processing in bounded batches keeps each scheduled run predictable under a large backlog.
Related explainers
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
java
public static Map<String, String> parseCookieHeader(String header) { Map<String, String> cookies = new LinkedHashMap<>(); if (header == null || header.isBlank()) { return cookies;
Parsing an HTTP Cookie header in Java
string-parsing
http
url-decoding
Intermediate
6 steps
java
public class TimedSocketReader { private static final int READ_TIMEOUT_MS = 5_000; private static final int CONNECT_TIMEOUT_MS = 3_000;
Reading a socket with connect and read timeouts
sockets
timeouts
io
Intermediate
8 steps
java
public final class EncodingDetector { public enum Encoding { UTF_8, UTF_16LE, UTF_16BE, UTF_32LE, UTF_32BE, ASCII, UNKNOWN
Detecting text encoding from raw bytes in Java
byte-manipulation
encoding-detection
bitwise-operations
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/retrying-failed-jobs-on-a-spring-schedule-explained-java-a35e/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.