How to Implement Delayed Tasks in Java: From Quartz to Redis and Beyond

This article compares delayed and scheduled tasks, explains their differences, and presents five practical Java implementations—including Quartz polling, JDK DelayQueue, Netty's HashedWheelTimer, Redis ZSET, and Redis keyspace notifications—along with their advantages, drawbacks, and sample code.

Java Interview Crash Guide
Java Interview Crash Guide
Java Interview Crash Guide
How to Implement Delayed Tasks in Java: From Quartz to Redis and Beyond

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

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

Scheduled tasks run periodically; delayed tasks run once after an event.

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

Solution Analysis

(1) Database Polling

Suitable for small projects: a thread periodically scans the database for overdue orders and updates or deletes them.

Implementation using Quartz:

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.2.2</version>
</dependency>
package com.rjzheng.delay1;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

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();
    }
}

Pros: simple, cluster‑friendly. Cons: high memory consumption, latency up to the scan interval, heavy DB load under large order volumes.

(2) JDK DelayQueue

Uses the built‑in DelayQueue, an unbounded blocking queue that releases elements only after their delay expires. Elements must implement Delayed.

package com.rjzheng.delay2;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class OrderDelay implements Delayed {
    private String orderId;
    private long timeout;
    OrderDelay(String orderId, long timeout) {
        this.orderId = orderId;
        this.timeout = timeout + System.nanoTime();
    }
    public int compareTo(Delayed other) {
        if (other == this) return 0;
        OrderDelay t = (OrderDelay) other;
        long d = getDelay(TimeUnit.NANOSECONDS) - t.getDelay(TimeUnit.NANOSECONDS);
        return d == 0 ? 0 : (d < 0 ? -1 : 1);
    }
    public long getDelay(TimeUnit unit) {
        return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS);
    }
    void print() {
        System.out.println(orderId + " order will be deleted...");
    }
}
package com.rjzheng.delay2;

import java.util.*;
import java.util.concurrent.*;

public class DelayQueueDemo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("00000001","00000002","00000003","00000004","00000005");
        DelayQueue<OrderDelay> queue = new DelayQueue<>();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 5; i++) {
            queue.put(new OrderDelay(list.get(i), TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS)));
            try {
                queue.take().print();
                System.out.println("After " + (System.currentTimeMillis() - start) + " Milliseconds");
            } catch (InterruptedException e) { e.printStackTrace(); }
        }
    }
}

Pros: high efficiency, low trigger latency. Cons: data lost on JVM restart, difficult cluster expansion, possible OOM under massive pending orders, higher code complexity.

(3) Time Wheel Algorithm

Imitates a clock where each tick moves the pointer; tasks are placed in slots based on their execution time. Netty’s HashedWheelTimer provides an implementation.

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 MyTimerTask(boolean flag) { this.flag = flag; }
        public void run(Timeout timeout) throws Exception {
            System.out.println("Deleting order from DB...");
            flag = false;
        }
    }
    public static void main(String[] argv) {
        MyTimerTask timerTask = new MyTimerTask(true);
        Timer timer = new HashedWheelTimer();
        timer.newTimeout(timerTask, 5, TimeUnit.SECONDS);
        int i = 1;
        while (timerTask.flag) {
            try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(i + " seconds passed");
            i++;
        }
    }
}

Pros: high efficiency, lower trigger latency than DelayQueue, simpler code. Cons: data lost on restart, cluster expansion difficulty, possible OOM under heavy load.

(4) Redis Cache

Uses Redis sorted set (ZSET) where the score stores the expiration timestamp and the member stores the order ID. A consumer periodically checks the first element and processes overdue orders.

package com.rjzheng.delay4;

import redis.clients.jedis.*;
import java.util.*;

public class AppTest {
    private static final String ADDR = "127.0.0.1";
    private static final int PORT = 6379;
    private static JedisPool jedisPool = new JedisPool(ADDR, PORT);
    public static Jedis getJedis() { return jedisPool.getResource(); }
    public void productionDelayMessage() {
        for (int i = 0; i < 5; i++) {
            Calendar cal = Calendar.getInstance();
            cal.add(Calendar.SECOND, 3);
            int ts = (int) (cal.getTimeInMillis() / 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, 1);
            if (items == null || items.isEmpty()) {
                System.out.println("No pending tasks");
                try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
                continue;
            }
            int score = (int) ((Tuple) items.toArray()[0]).getScore();
            int now = (int) (Calendar.getInstance().getTimeInMillis() / 1000);
            if (now >= score) {
                String orderId = ((Tuple) items.toArray()[0]).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 app = new AppTest();
        app.productionDelayMessage();
        app.consumerDelayMessage();
    }
}

Pros: messages persisted in Redis, easy cluster scaling, high time accuracy. Cons: requires Redis maintenance.

(5) Redis Keyspace Notifications

Enables callbacks when a key expires. Add notify-keyspace-events Ex to redis.conf and subscribe to the __keyevent@0__:expired channel.

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 JedisPool jedis = new JedisPool(ADDR, PORT);
    private static RedisSub sub = new RedisSub();
    public static void init() {
        new Thread(() -> jedis.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;
            jedis.getResource().setex(orderId, 3, orderId);
            System.out.println(System.currentTimeMillis() + "ms:" + orderId + " created");
        }
    }
    static class RedisSub extends JedisPubSub {
        public void onMessage(String channel, String message) {
            System.out.println(System.currentTimeMillis() + "ms:" + message + " order cancelled");
        }
    }
}

Note: Redis Pub/Sub is fire‑and‑forget; if the subscriber disconnects, events are lost, making this approach less reliable.

(6) Message Queue (RabbitMQ)

RabbitMQ supports delayed queues via message TTL ( x-message-ttl) and dead‑letter exchange routing ( x-dead-letter-exchange, x-dead-letter-routing-key), enabling reliable delayed processing with persistence and horizontal scaling.

Pros: high efficiency, distributed, persistent messages. Cons: added operational complexity and cost due to RabbitMQ management.

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.

JavaredisSchedulingdelayed tasksQuartzDelayQueue
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.