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

This article examines the concept of delayed tasks versus scheduled tasks, outlines their differences, and presents five practical implementation strategies—including database polling with Quartz, JDK DelayQueue, Netty’s HashedWheelTimer, Redis ZSET and keyspace notifications, and RabbitMQ delayed queues—complete with code samples, performance pros and cons, and scalability considerations.

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

Background

In many systems a task must be executed after a certain delay triggered by an event, e.g., cancel an unpaid order after 30 minutes or send an SMS 60 seconds after order creation. Unlike scheduled jobs, delayed tasks have no fixed trigger time, no periodic execution and usually operate on a single item.

Solution 1 – Database Polling (Quartz)

Periodically scan the order table for records whose creation time exceeds a timeout and update or delete them. Simple to implement and works in clustered environments.

Maven dependency

org.quartz-scheduler
quartz
2.2.2

Implementation

package com.rjzheng.delay1;

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

public class MyJob implements Job {
    @Override
    public void execute(JobExecutionContext context) {
        System.out.println("Scanning database for expired orders…");
    }

    public static void main(String[] args) throws Exception {
        JobDetail job = 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(job, trigger);
        scheduler.start();
    }
}

Running the program prints the message every three seconds.

Pros & Cons

Pros: Very simple, easy to understand, supports clustering via Quartz.

Cons: High memory usage, latency up to the scan interval, heavy DB load when order volume is large.

Solution 2 – JDK DelayQueue

Use java.util.concurrent.DelayQueue, an unbounded blocking queue that releases elements only after their delay expires. Elements must implement java.util.concurrent.Delayed.

Delayed element

package com.rjzheng.delay2;

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

public class OrderDelay implements Delayed {
    private final String orderId;
    private final long expireAt; // nanoseconds

    public OrderDelay(String orderId, long delay, TimeUnit unit) {
        this.orderId = orderId;
        this.expireAt = System.nanoTime() + unit.toNanos(delay);
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(expireAt - System.nanoTime(), TimeUnit.NANOSECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        if (o == this) return 0;
        long diff = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
        return (diff == 0) ? 0 : (diff < 0 ? -1 : 1);
    }

    public void process() {
        System.out.println(orderId + " – order expired, delete it.");
    }
}

Demo

package com.rjzheng.delay2;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.TimeUnit;

public class DelayQueueDemo {
    public static void main(String[] args) {
        List<String> ids = List.of("00000001","00000002","00000003","00000004","00000005");
        DelayQueue<OrderDelay> queue = new DelayQueue<>();
        long start = System.currentTimeMillis();
        for (String id : ids) {
            queue.put(new OrderDelay(id, 3, TimeUnit.SECONDS));
            try {
                OrderDelay d = queue.take();
                d.process();
                System.out.println("After " + (System.currentTimeMillis() - start) + " ms");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

The program prints each order after a three‑second delay.

Pros & Cons

Pros: High efficiency, low trigger latency.

Cons: Data lost on JVM restart, difficult to scale, possible OOM when many pending orders, higher code complexity.

Solution 3 – Time Wheel (Netty HashedWheelTimer )

A timing wheel works like a clock: a pointer moves one tick at a fixed interval. Important parameters are ticksPerWheel, tickDuration and timeUnit. When the pointer reaches a slot, all tasks in that slot are executed.

Maven dependency

io.netty
netty-all
4.1.24.Final

Implementation

package com.rjzheng.delay3;

import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import java.util.concurrent.TimeUnit;

public class HashedWheelTimerDemo {
    static class MyTask implements TimerTask {
        private volatile boolean running = true;
        @Override
        public void run(Timeout timeout) {
            System.out.println("Deleting expired order from DB…");
            running = false;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyTask task = new MyTask();
        HashedWheelTimer timer = new HashedWheelTimer();
        timer.newTimeout(task, 5, TimeUnit.SECONDS);
        int seconds = 0;
        while (task.running) {
            Thread.sleep(1000);
            System.out.println(++seconds + " seconds passed");
        }
    }
}

Output shows a second counter and the task execution after five seconds.

Pros & Cons

Pros: Efficient, lower latency than DelayQueue, code is relatively simple.

Cons: Same restart‑data‑loss issue, scaling difficulty, possible OOM with massive tasks.

Solution 4 – Redis Sorted Set (ZSET)

Store each order ID as a member and its expiration timestamp (seconds) as the score in a Redis ZSET. A consumer repeatedly queries the ZSET for elements whose score ≤ current time and removes them.

Implementation

package com.rjzheng.delay4;

import java.util.Calendar;
import java.util.Set;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Tuple;

public class RedisZSetDemo {
    private static final String HOST = "127.0.0.1";
    private static final int PORT = 6379;
    private static final JedisPool POOL = new JedisPool(HOST, PORT);

    private static Jedis jedis() { return POOL.getResource(); }

    // Produce 5 orders with a 3‑second delay
    public void produce() {
        for (int i = 0; i < 5; i++) {
            Calendar cal = Calendar.getInstance();
            cal.add(Calendar.SECOND, 3);
            long ts = cal.getTimeInMillis() / 1000;
            jedis().zadd("OrderId", ts, "OID000000" + i);
            System.out.println(System.currentTimeMillis() + " ms: created order OID000000" + i);
        }
    }

    // Consume expired orders
    public void consume() {
        while (true) {
            Set<Tuple> items = jedis().zrangeWithScores("OrderId", 0, 0);
            if (items.isEmpty()) {
                try { Thread.sleep(500); } catch (InterruptedException ignored) {}
                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) {
        RedisZSetDemo demo = new RedisZSetDemo();
        demo.produce();
        demo.consume();
    }
}

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

Concurrency fix : check the return value of zrem; only process the order when the removal count is greater than zero (as shown above).

Pros & Cons

Pros: High efficiency, persistence guarantees reliability, easy horizontal scaling with Redis cluster, accurate timing.

Cons: Requires a Redis deployment and its operational overhead.

Solution 5 – Redis Keyspace Notifications

Enable keyspace events (e.g., notify-keyspace-events Ex) so that when a key expires Redis publishes a message on the __keyevent@0__:expired channel. A subscriber receives the event and performs order cancellation.

Implementation

package com.rjzheng.delay5;

import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPubSub;

public class RedisKeyspaceDemo {
    private static final String HOST = "127.0.0.1";
    private static final int PORT = 6379;
    private static final JedisPool POOL = new JedisPool(HOST, PORT);

    public static void main(String[] args) throws InterruptedException {
        // Start subscriber thread
        new Thread(() -> {
            try (var jedis = POOL.getResource()) {
                jedis.subscribe(new JedisPubSub() {
                    @Override
                    public void onMessage(String channel, String message) {
                        System.out.println(System.currentTimeMillis() + " ms: order " + message + " cancelled");
                    }
                }, "__keyevent@0__:expired");
            }
        }).start();

        // Produce test keys with 3‑second TTL
        for (int i = 0; i < 10; i++) {
            String orderId = "OID000000" + i;
            try (var jedis = POOL.getResource()) {
                jedis.setex(orderId, 3, orderId);
                System.out.println(System.currentTimeMillis() + " ms: created " + orderId);
            }
        }
    }
}

Each order is cancelled three seconds after creation.

Pros & Cons

Pros: Events are stored in Redis, so a consumer crash does not lose them; easy to scale; accurate timing.

Cons: Requires additional Redis configuration and maintenance.

Solution 6 – RabbitMQ Delayed Queue

Use RabbitMQ per‑queue or per‑message TTL (e.g., x-message-ttl) together with a dead‑letter exchange/routing‑key. When the TTL expires the message is dead‑lettered to a consumer queue, achieving delayed delivery.

Pros & Cons

Pros: High efficiency, leverages RabbitMQ clustering for horizontal scaling, supports message persistence for reliability.

Cons: Adds 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.

JavaBackend DevelopmentredisSchedulingRabbitMQdelayed tasksQuartz
IT Architects Alliance
Written by

IT Architects Alliance

Discussion and exchange on system, internet, large‑scale distributed, high‑availability, and high‑performance architectures, as well as big data, machine learning, AI, and architecture adjustments with internet technologies. Includes real‑world large‑scale architecture case studies. Open to architects who have ideas and enjoy sharing.

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.