Recreating Taobao’s 30‑Minute Unpaid Order Auto‑Close in Spring Boot Without Redis or Minute‑by‑Minute Scans

The article explains how to replace the common @Scheduled per‑minute scan or Redis TTL approach for auto‑closing unpaid orders with JobRunr’s durable delayed‑job feature in Spring Boot, detailing the implementation steps, scalability benefits, idempotent handling, and distributed execution considerations.

LuTiao Programming
LuTiao Programming
LuTiao Programming
Recreating Taobao’s 30‑Minute Unpaid Order Auto‑Close in Spring Boot Without Redis or Minute‑by‑Minute Scans

This week Spring’s "This Week in Spring" highlighted JobRunr, a Java background‑task framework that persists, retries, and delays method execution. The author uses it to implement the familiar e‑commerce feature of automatically closing orders that remain unpaid for 30 minutes.

1. The naive @Scheduled scan

Many developers start with a scheduled method that runs every minute:

@Scheduled(fixedDelay = 60_000)
public void closeExpiredOrders() {
    orderRepository.closeExpiredOrders();
}

SQL to close orders:

UPDATE orders
SET status = 'CLOSED'
WHERE status = 'UNPAID'
  AND created_at < NOW() - INTERVAL 30 MINUTE;

While this works for small systems, it becomes inefficient when order volume grows (e.g., 1 million orders per day) because each minute the database scans many rows that do not need processing. Scaling to multiple instances adds complexity: each instance would run the scan, requiring Redis or database locks (ShedLock) to avoid duplicate execution.

2. A more natural approach: schedule a job when the order is created

Instead of polling, create a delayed JobRunr task at order creation time. The task’s execution time is the order’s creation timestamp plus 30 minutes:

Instant closeAt = Instant.now().plus(30, ChronoUnit.MINUTES);
jobScheduler.schedule(closeAt, () -> closeUnpaidOrder(order.getId()));

This model aligns directly with business logic: each order carries its own expiration time, and the job persists across application restarts.

3. Adding JobRunr to Spring Boot

With Spring Boot 4, add the official starter:

org.jobrunr:jobrunr-spring-boot-4-starter:8.8.1

Enable the background‑job server and dashboard in application.yml:

jobrunr:
  background-job-server:
    enabled: true
  dashboard:
    enabled: true
    port: 8000

Use a durable storage (MySQL, PostgreSQL, etc.) so jobs survive JVM restarts.

4. Idempotent close logic

The job must only close an order if it is still unpaid. The SQL update is written as a conditional statement:

UPDATE orders
SET status = 'CLOSED', closed_at = NOW()
WHERE id = :orderId AND status = 'UNPAID';

Java service checks the affected row count:

@Transactional
public void closeUnpaidOrder(Long orderId) {
    int affected = orderRepository.closeIfUnpaid(orderId);
    if (affected == 1) {
        releaseInventory(orderId);
        publishOrderClosedEvent(orderId);
    }
}

If the user has already paid, the update affects zero rows, and JobRunr does nothing else.

5. Do not delete the delayed job on payment

Removing the scheduled job after a successful payment introduces a new consistency problem: the removal could fail if the service crashes, leaving the job to run later. Instead, let the job run; its idempotent check ensures no harmful action.

6. Distributed execution without extra locks

Running ten Spring Boot instances does not require ten Redis locks. All instances share the same JobRunr storage, and the built‑in job server distributes tasks among them. The dashboard (e.g., http://server:8000) shows task states (Scheduled, Enqueued, Processing, Succeeded, Failed), simplifying troubleshooting.

7. Failure retry and idempotency

JobRunr automatically retries failed jobs. Therefore, business code must be safe to execute multiple times: inventory release, coupon sending, and message publishing must each be idempotent (e.g., using outbox patterns).

8. When to consider canceling a job

Only if the job is extremely expensive should you consider explicit cancellation; otherwise, a harmless idempotent retry is simpler.

9. Conclusion

The shift from cron‑style scans to durable delayed execution makes background processing more reliable, scalable, and easier to reason about. JobRunr’s persistent jobs fit many scenarios such as order timeout, appointment cancellation, welcome‑email scheduling, membership expiry, delayed refunds, batch data processing, and long‑running AI tasks.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

distributed schedulingspring-bootidempotencydelayed tasksbackground jobsJobRunr
LuTiao Programming
Written by

LuTiao Programming

LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.