Mastering Delayed Tasks: 5 Proven Java Backend Solutions

This article compares delayed and scheduled tasks, then details five practical backend implementations—including database polling with Quartz, JDK DelayQueue, Netty time‑wheel, Redis ZSET (and keyspace notifications), and RabbitMQ delayed queues—complete with code samples, advantages, and drawbacks.

Java Interview Crash Guide
Java Interview Crash Guide
Java Interview Crash Guide
Mastering Delayed Tasks: 5 Proven Java Backend Solutions

In development, delayed tasks such as automatically canceling an unpaid order after 30 minutes or sending an SMS 60 seconds after order creation are common.

The term delayed task differs from a scheduled task in three main ways:

Scheduled tasks have a fixed trigger time; delayed tasks do not.

Scheduled tasks run periodically, while delayed tasks execute once after an event.

Scheduled tasks usually handle batch operations; delayed tasks typically handle a single operation.

Solution Analysis

(1) Database Polling

Idea

This approach uses a dedicated thread to periodically scan the database for overdue orders and then updates or deletes them.

Implementation

Add Quartz to the Maven project:

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.2.2</version>
</dependency>

Demo job class:

package com.rjzheng.delay1;
import org.quartz.*;
public class MyJob implements Job {
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("Scanning database for overdue orders…");
    }
    public static void main(String[] args) throws Exception {
        JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
            .withIdentity("job1", "group1").build();
        Trigger trigger = TriggerBuilder.newTrigger()
            .withIdentity("trigger1", "group3")
            .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInSeconds(3).repeatForever())
            .build();
        Scheduler scheduler = new StdSchedulerFactory().getScheduler();
        scheduler.scheduleJob(jobDetail, trigger);
        scheduler.start();
    }
}

Running the job prints the message every 3 seconds.

Pros

Simple to implement and supports clustering.

Cons

Consumes significant server memory.

Maximum delay equals the polling interval (e.g., up to 3 seconds).

Scanning large tables frequently can heavily load the database.

(2) JDK DelayQueue

Idea

Uses the unbounded blocking queue DelayQueue, which only releases elements after their delay expires. Elements must implement Delayed.

Implementation

package com.rjzheng.delay2;
import java.util.concurrent.*;
public class OrderDelay implements Delayed {
    private String orderId;
    private long timeout;
    public OrderDelay(String orderId, long timeout) {
        this.orderId = orderId;
        this.timeout = timeout + System.nanoTime();
    }
    public int compareTo(Delayed other) { /* omitted for brevity */ }
    public long getDelay(TimeUnit unit) {
        return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS);
    }
    public void print() {
        System.out.println(orderId + " order will be deleted…");
    }
}
package com.rjzheng.delay2;
import java.util.concurrent.*;
public class DelayQueueDemo {
    public static void main(String[] args) throws Exception {
        DelayQueue<OrderDelay> queue = new DelayQueue<>();
        for (int i = 0; i < 5; i++) {
            queue.put(new OrderDelay("0000000" + (i + 1),
                TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS)));
            System.out.println(queue.take().print());
        }
    }
}

The demo shows each order being processed after a 3‑second delay.

Pros

High efficiency with low task‑trigger latency.

Cons

Data is lost if the server restarts.

Cluster expansion is cumbersome.

Large numbers of pending orders can cause OOM errors.

Code complexity is relatively high.

(3) Time‑Wheel Algorithm

Idea

The algorithm works like a clock: a pointer (tick) moves at a fixed frequency. Each tick represents a time slice; tasks are placed into slots based on their delay.

Implementation

Add Netty’s HashedWheelTimer dependency:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.24.Final</version>
</dependency>
package com.rjzheng.delay3;
import io.netty.util.*;
import java.util.concurrent.TimeUnit;
public class HashedWheelTimerTest {
    static class MyTimerTask implements TimerTask {
        boolean flag = true;
        public void run(Timeout timeout) throws Exception {
            System.out.println("Deleting order from DB…");
            flag = false;
        }
    }
    public static void main(String[] args) {
        MyTimerTask task = new MyTimerTask();
        Timer timer = new HashedWheelTimer();
        timer.newTimeout(task, 5, TimeUnit.SECONDS);
        while (task.flag) {
            try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("One second passed");
        }
    }
}

The output shows the timer counting seconds and executing the task after 5 seconds.

Pros

High efficiency with lower latency than DelayQueue and simpler code.

Cons

Data is lost on server restart.

Cluster scaling is difficult.

Large numbers of pending tasks may cause OOM.

(4) Redis Cache

Idea 1 – Sorted Set (ZSET)

Store order IDs as members and expiration timestamps as scores. Periodically query the first element to see if it has expired.

package com.rjzheng.delay4;
import redis.clients.jedis.*;
public class AppTest {
    private static final String ADDR = "127.0.0.1";
    private static final int PORT = 6379;
    private static final JedisPool pool = new JedisPool(ADDR, PORT);
    public static Jedis getJedis() { return pool.getResource(); }
    public void productionDelayMessage() {
        for (int i = 0; i < 5; i++) {
            long ts = (System.currentTimeMillis() + 3000) / 1000;
            getJedis().zadd("OrderId", ts, "OID0000001" + i);
            System.out.println(System.currentTimeMillis() + "ms: generated order OID0000001" + i);
        }
    }
    public void consumerDelayMessage() {
        Jedis jedis = getJedis();
        while (true) {
            Set<Tuple> items = jedis.zrangeWithScores("OrderId", 0, 0);
            if (items == null || items.isEmpty()) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } continue; }
            Tuple tuple = items.iterator().next();
            long score = (long) tuple.getScore();
            long now = System.currentTimeMillis() / 1000;
            if (now >= score) {
                String orderId = tuple.getElement();
                Long removed = jedis.zrem("OrderId", orderId);
                if (removed != null && removed > 0) {
                    System.out.println(System.currentTimeMillis() + "ms: consumed order " + orderId);
                }
            }
        }
    }
    public static void main(String[] args) {
        AppTest t = new AppTest();
        t.productionDelayMessage();
        t.consumerDelayMessage();
    }
}

Running the program shows each order being consumed roughly 3 seconds after creation.

Issue & Fix

In high‑concurrency scenarios multiple consumers may process the same order. The fix checks the return value of zrem and only processes when the removal count is greater than zero.

Idea 2 – Keyspace Notifications

Configure Redis with notify-keyspace-events Ex to receive an event when a key expires.

package com.rjzheng.delay5;
import redis.clients.jedis.*;
public class RedisTest {
    private static final String ADDR = "127.0.0.1";
    private static final int PORT = 6379;
    private static final JedisPool pool = new JedisPool(ADDR, PORT);
    private static final RedisSub sub = new RedisSub();
    public static void init() {
        new Thread(() -> pool.getResource().subscribe(sub, "__keyevent@0__:expired")).start();
    }
    public static void main(String[] args) throws InterruptedException {
        init();
        for (int i = 0; i < 10; i++) {
            String orderId = "OID000000" + i;
            pool.getResource().setex(orderId, 3, orderId);
            System.out.println(System.currentTimeMillis() + "ms:" + orderId + " order created");
        }
    }
    static class RedisSub extends JedisPubSub {
        @Override
        public void onMessage(String channel, String message) {
            System.out.println(System.currentTimeMillis() + "ms:" + message + " order cancelled");
        }
    }
}

The output shows each order being cancelled exactly 3 seconds after creation.

Pros

Messages are persisted in Redis, allowing recovery after crashes.

Easy to scale horizontally.

High timing accuracy.

Cons

Requires additional Redis maintenance.

(5) Message Queue (RabbitMQ)

RabbitMQ can implement delayed queues using message TTL ( x-message-ttl) and dead‑letter exchange routing ( x-dead-letter-exchange, x-dead-letter-routing-key).

Pros & Cons

High efficiency, supports distributed deployment, and messages can be persisted for reliability.

Requires RabbitMQ operations and adds operational complexity and cost.

Conclusion

The article summarizes the most common delayed‑task implementations in modern internet systems, providing code samples, advantages, and disadvantages for each approach.

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.

Java Interview Crash Guide
Written by

Java Interview Crash Guide

Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.

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.