How to Auto‑Cancel Unpaid Orders After 30 Minutes: Design and Implementation Options
The article explains the concept of delayed tasks versus scheduled tasks and evaluates several backend solutions—including database polling with Quartz, JDK DelayQueue, Netty's HashedWheelTimer, Redis ZSET, Redis key‑space notifications, and RabbitMQ delayed queues—detailing their implementations, code samples, advantages, and drawbacks for automatically cancelling orders that remain unpaid for a set period.
In many e‑commerce systems an order that is not paid within a certain time (e.g., 30 minutes) must be cancelled automatically. The author first defines this requirement as a delayed task and contrasts it with a traditional scheduled task :
Scheduled tasks have a fixed trigger time; delayed tasks are triggered a certain interval after an event.
Scheduled tasks may run periodically; delayed tasks run once after the delay.
Scheduled tasks often handle batch operations; delayed tasks usually handle a single item.
Approach 1 – Database polling (Quartz)
The simplest method for small projects is to start a thread that periodically scans the order table and updates or deletes rows whose creation timestamp exceeds the timeout. The author shows a Maven dependency for Quartz 2.2.2 and a demo job:
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.2</version>
</dependency> 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 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();
}
}Pros: simple, works in a cluster. Cons: high memory consumption, latency up to the scan interval, and heavy DB load when order volume is large.
Approach 2 – JDK DelayQueue
This solution uses the built‑in DelayQueue<Delayed>, an unbounded blocking queue that releases elements only after their delay expires. The author provides the OrderDelay class implementing Delayed and a demo that enqueues five orders with a 3‑second delay.
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 o = (OrderDelay) other;
long diff = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
return diff == 0 ? 0 : (diff < 0 ? -1 : 1);
}
public long getDelay(TimeUnit unit) {
return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS);
}
void print() { System.out.println(orderId + " will be deleted…"); }
} 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) + " ms");
} catch (InterruptedException e) {}
}
}
}Output shows each order being deleted after roughly 3 seconds. Pros: high efficiency, low latency. Cons: data loss on server restart, difficult cluster expansion, possible OOM when many delayed orders exist, and higher code complexity.
Approach 3 – Netty HashedWheelTimer
The author introduces Netty’s HashedWheelTimer, which uses a time‑wheel (similar to a clock) to schedule tasks with low overhead. Maven dependency:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.24.Final</version>
</dependency> public class HashedWheelTimerTest {
static class MyTimerTask implements TimerTask {
boolean flag;
MyTimerTask(boolean flag) { this.flag = flag; }
public void run(Timeout timeout) {
System.out.println("Deleting order…");
flag = false;
}
}
public static void main(String[] args) {
MyTimerTask task = new MyTimerTask(true);
Timer timer = new HashedWheelTimer();
timer.newTimeout(task, 5, TimeUnit.SECONDS);
int i = 1;
while (task.flag) {
Thread.sleep(1000);
System.out.println(i + " seconds passed");
i++;
}
}
}Pros: efficient, lower trigger latency than DelayQueue, simpler code. Cons: same restart‑data‑loss issue and cluster scaling difficulty.
Approach 4 – Redis Sorted Set (ZSET)
Using Redis ZSET, the order ID is stored as the member and the expiration timestamp as the score. The consumer repeatedly reads the smallest score, compares it with the current time, and removes the element if it has expired.
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(); }
// producer
public void productionDelayMessage() {
for (int i = 0; i < 5; i++) {
Calendar c = Calendar.getInstance();
c.add(Calendar.SECOND, 3);
int ts = (int) (c.getTimeInMillis() / 1000);
getJedis().zadd("OrderId", ts, "OID000000" + i);
System.out.println(System.currentTimeMillis() + " ms: generated order OID000000" + 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");
Thread.sleep(500);
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 t = new AppTest();
t.productionDelayMessage();
t.consumerDelayMessage();
}
}Pros: high efficiency, low latency, easy cluster expansion. Cons: requires Redis maintenance, and the Pub/Sub variant (see below) suffers from reliability issues.
Approach 5 – Redis Key‑Space Notifications
Redis can publish an event when a key expires. By enabling notify-keyspace-events Ex in redis.conf, a subscriber receives a callback and can delete the order.
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: generated " + orderId);
}
}
static class RedisSub extends JedisPubSub {
public void onMessage(String channel, String message) {
System.out.println(System.currentTimeMillis() + " ms: " + message + " cancelled");
}
}
}Pros: messages survive process crashes, easy horizontal scaling, accurate timing. Cons: relies on Redis, and Pub/Sub is fire‑and‑forget, so events can be lost if the subscriber disconnects.
Approach 6 – RabbitMQ Delayed Queue
RabbitMQ supports per‑message TTL ( x-message-ttl) and dead‑letter exchanges. By configuring a queue with x-dead-letter-exchange and x-dead-letter-routing-key, expired messages are routed to a consumer that cancels the order. The author notes that a full implementation will be covered in a future article.
Pros: high efficiency, built‑in clustering, persistent messages increase reliability. Cons: adds operational complexity and cost because RabbitMQ must be managed.
Overall, the article walks through each solution’s design, provides runnable Java examples, and lists concrete advantages and drawbacks, helping readers choose the most suitable delayed‑task mechanism for order‑cancellation scenarios.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
