Mastering Delayed Tasks in Java: From Quartz to Redis and RabbitMQ

This article compares delayed and scheduled tasks, explains their differences, and presents five practical Java implementations—including database polling with Quartz, JDK DelayQueue, Netty's HashedWheelTimer, Redis sorted sets, and RabbitMQ—highlighting each method's advantages and drawbacks.

Programmer DD
Programmer DD
Programmer DD
Mastering Delayed Tasks in Java: From Quartz to Redis and RabbitMQ

Introduction

In many applications you need to execute actions after a certain delay, such as automatically cancelling an order that has not been paid for 30 minutes or sending an SMS 60 seconds after order creation. These are called delayed tasks, which differ from scheduled tasks in trigger time, execution cycle, and granularity.

Differences between Delayed and Scheduled Tasks

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.

Implementation Options

1. Database Polling (Quartz)

This approach is common in small projects: a thread periodically scans the database for overdue orders and updates or deletes them.

Dependency

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

Demo Job

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...");
    }

    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, easy to use, supports clustering.

Cons: high memory consumption, latency up to the scan interval, heavy database load for large order volumes.

2. JDK DelayQueue

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

OrderDelay Class

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...");
    }
}

Demo

package com.rjzheng.delay2;

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

public class DelayQueueDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("00000001");
        list.add("00000002");
        list.add("00000003");
        list.add("00000004");
        list.add("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();
            }
        }
    }
}

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

Pros: high efficiency, low latency.

Cons: data lost on server restart, difficult to scale in a cluster, possible OOM for massive order volumes, higher code complexity.

3. Time‑Wheel Algorithm (Netty HashedWheelTimer)

The time‑wheel works like a clock: a pointer advances at a fixed tick duration, and tasks are placed into slots based on their execution time.

Maven Dependency

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.24.Final</version>
</dependency>

Demo

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 database...");
            this.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 latency than DelayQueue, simpler code.

Cons: tasks disappear after server restart, scaling challenges, OOM risk for huge order sets.

4. Redis Sorted Set (ZSET)

Redis ZSET stores members with a score; the score can be the Unix timestamp when the order should expire.

Key Commands

ZADD key score member

ZRANGE key start stop [WITHSCORES]

ZSCORE key member

ZREM key member

Implementation

package com.rjzheng.delay4;

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

public class AppTest {
    private static final String ADDR = "127.0.0.1";
    private static final int PORT = 6379;
    private static final JedisPool jedisPool = new JedisPool(ADDR, PORT);

    public static Jedis getJedis() { return jedisPool.getResource(); }

    // Producer
    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);
        }
    }

    // Consumer
    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: high efficiency, low latency, easy cluster expansion, reliable because data persists in Redis.

Cons: requires Redis maintenance.

Redis Keyspace Notifications (Alternative)

Configure notify-keyspace-events Ex in redis.conf 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 jedis = new JedisPool(ADDR, PORT);
    private static final 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 + " generated");
        }
    }

    static class RedisSub extends JedisPubSub {
        @Override
        public void onMessage(String channel, String message) {
            System.out.println(System.currentTimeMillis() + " ms:" + message + " cancelled");
        }
    }
}

Pros: messages are stored in Redis, so they survive process crashes; easy horizontal scaling; accurate timing.

Cons: additional Redis maintenance; Pub/Sub is fire‑and‑forget, so events can be lost if the subscriber disconnects.

5. Message Queue (RabbitMQ)

RabbitMQ can implement delayed queues using x-message-ttl and dead‑letter exchanges. This provides reliable, persistent delayed messages with built‑in clustering.

Pros: high performance, reliable persistence, easy horizontal scaling.

Cons: operational overhead of managing RabbitMQ, increased complexity and cost.

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.

JavaBackend DevelopmentredisSchedulingRabbitMQdelayed tasks
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

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.