Back to Reactive: Vert.x & RSocket
Q7Question 7 of 8

Can you run batch jobs in Vert.x (setPeriodic, worker verticles/executeBlocking, Quartz cron)?

Key Concepts
  • Vert.x supports batch jobs with setPeriodic for fixed intervals and setTimer for one-shot delays, both firing on the event loop.
  • The timer handler must not block; its only job is to kick off heavy work on a worker verticle or executeBlocking.
  • Worker verticles and executeBlocking run on the worker pool and may block, keeping the event loop free for live requests.
  • For calendar-style cron scheduling, integrate the Vert.x Quartz extension or compute the next fire time and arm a timer.
  • In a clustered deployment, use leader election or a distributed lock to prevent every node from running the same batch job.
  • Long batch jobs should be idempotent, chunked, and restartable so a crash mid-batch can be recovered safely.

Definition

Schedule the trigger on the event loop with setPeriodic or a cron timer, but always run the heavy batch work on a worker verticle or executeBlocking.
  • setPeriodic – fires a handler on the event loop at a fixed interval and returns a timer id you can cancel.
  • setTimer – fires a handler once after a delay, useful for one-shot scheduling.
  • executeBlocking – runs a blocking lambda on the worker thread pool and returns a Future, keeping the event loop free.
  • Worker verticle – a verticle deployed with setWorker(true) that runs entirely on worker threads and may block.
  • Cron scheduling – for calendar-based triggers (every weekday at 2 AM), integrate Quartz or compute the next fire time and arm a timer.

Imagine National Bank of India must run an end-of-day interest posting job. The fast front-desk clerk (event loop) is not the one who should sit crunching thousands of accounts because that would freeze the counter. Instead, the clerk keeps a timer on his desk (setPeriodic) that rings every interval, and when it rings he hands the heavy ledger work to the back-office team (worker verticle) so the counter keeps serving customers. For jobs that must run at a specific wall-clock time like 2 AM on business days, the bank uses a calendar scheduler (Quartz cron) that wakes the back office at exactly the right moment.

Understanding the Concepts

How does setPeriodic scheduling work?

For simple fixed-rate scheduling, vertx.setPeriodic(interval, handler) fires every interval milliseconds and returns a timer id you can cancel. The handler executes on the event loop, so it must not block.

JAVA
package com.bank.batch;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;

public class PeriodicTrigger extends AbstractVerticle {
    @Override
    public void start() {
        // Fire every 60 seconds
        long timerId = vertx.setPeriodic(60_000, id -> {
            System.out.println("Trigger fired, delegating to worker...");
            // Kick off heavy work on the worker pool
            vertx.executeBlocking(promise -> {
                reconcileAccounts();
                promise.complete();
            });
        });

        System.out.println("Reconciliation scheduled, timer id: " + timerId);

        // Cancel after 10 minutes
        vertx.setTimer(600_000, id -> {
            vertx.cancelTimer(timerId);
            System.out.println("Timer cancelled");
        });
    }

    private void reconcileAccounts() {
        System.out.println("Reconciling accounts for National Bank of India...");
    }

    public static void main(String[] args) {
        Vertx.vertx().deployVerticle(new PeriodicTrigger());
    }
}
TEXT
Reconciliation scheduled, timer id: 1
Trigger fired, delegating to worker...
Reconciling accounts for National Bank of India...

How do worker verticles handle batch processing?

A worker verticle is deployed with setWorker(true) and runs entirely on worker threads, so its handler may block. For batch jobs, you trigger it over the event bus.

JAVA
package com.bank.batch;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Vertx;
import java.math.BigDecimal;

public class InterestBatchVerticle extends AbstractVerticle {
    @Override
    public void start() {
        // This runs on a worker thread: blocking is allowed
        vertx.eventBus().consumer("batch.interest", msg -> {
            System.out.println("Starting interest calculation...");
            BigDecimal totalInterest = calculateDailyInterest();
            msg.reply(totalInterest.toString());
        });
    }

    private BigDecimal calculateDailyInterest() {
        // Simulates processing 10,000 accounts
        BigDecimal total = BigDecimal.ZERO;
        BigDecimal rate = new BigDecimal("0.04"); // 4% annual
        BigDecimal dailyRate = rate.divide(
                new BigDecimal("365"), 10, BigDecimal.ROUND_HALF_UP);

        for (int i = 0; i < 10_000; i++) {
            BigDecimal balance = new BigDecimal("100000.00");
            BigDecimal interest = balance.multiply(dailyRate);
            total = total.add(interest);
        }
        System.out.println("Processed 10,000 accounts");
        return total;
    }

    public static void main(String[] args) {
        Vertx vertx = Vertx.vertx();

        // Deploy as worker verticle
        vertx.deployVerticle(new InterestBatchVerticle(),
                new DeploymentOptions().setWorker(true))
                .onSuccess(id -> {
                    // Trigger the batch job over the event bus
                    vertx.eventBus().request("batch.interest", "", reply -> {
                        if (reply.succeeded()) {
                            System.out.println("Total interest: " + reply.result().body());
                        }
                    });
                });
    }
}
TEXT
Starting interest calculation...
Processed 10,000 accounts
Total interest: 10958.9041095890

How does executeBlocking differ from a worker verticle?

executeBlocking is simpler for one-off tasks, while a worker verticle is better for a dedicated long-running service.

JAVA
package com.bank.batch;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import io.vertx.core.WorkerExecutor;
import java.math.BigDecimal;

public class ExecuteBlockingDemo extends AbstractVerticle {
    @Override
    public void start() {
        // Dedicated worker pool for batch work
        WorkerExecutor batchPool = vertx.createSharedWorkerExecutor(
                "batch-pool", 10);

        // Run on the dedicated pool
        batchPool.executeBlocking(promise -> {
            BigDecimal result = processStatements();
            promise.complete(result);
        }).onSuccess(total ->
                System.out.println("Statement batch complete: " + total));
    }

    private BigDecimal processStatements() {
        System.out.println("Processing statements on thread: "
                + Thread.currentThread().getName());
        return new BigDecimal("5000000.00");
    }

    public static void main(String[] args) {
        Vertx.vertx().deployVerticle(new ExecuteBlockingDemo());
    }
}
TEXT
Processing statements on thread: vert.x-worker-thread-0
Statement batch complete: 5000000.00

How do you implement cron scheduling?

For calendar-style cron scheduling, setPeriodic is not enough because it only does fixed intervals. Parse a cron expression, compute the delay until the next fire time, and arm a timer.

JAVA
package com.bank.batch;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Duration;

public class CronScheduler extends AbstractVerticle {
    private static final LocalTime TARGET_TIME = LocalTime.of(2, 0); // 2 AM

    @Override
    public void start() {
        scheduleNext();
    }

    private void scheduleNext() {
        long delayMs = computeDelayUntilNext();
        System.out.println("Next batch in " + (delayMs / 1000) + " seconds");

        vertx.setTimer(delayMs, id -> {
            System.out.println("2 AM batch triggered at " + LocalDateTime.now());
            // Run heavy work on worker pool
            vertx.executeBlocking(promise -> {
                runEndOfDayBatch();
                promise.complete();
            }).onComplete(ar -> scheduleNext()); // re-arm
        });
    }

    private long computeDelayUntilNext() {
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime next = now.toLocalDate().atTime(TARGET_TIME);
        if (now.isAfter(next)) {
            next = next.plusDays(1);
        }
        return Duration.between(now, next).toMillis();
    }

    private void runEndOfDayBatch() {
        System.out.println("Running end-of-day interest posting "
                + "for National Bank of India...");
    }

    public static void main(String[] args) {
        Vertx.vertx().deployVerticle(new CronScheduler());
    }
}

What are common mistakes with batch jobs in Vert.x?

The most common mistake is running the batch work directly on the event loop.

JAVA
// WRONG: blocking the event loop with batch processing
vertx.setPeriodic(60_000, id -> {
    // This processes 100,000 accounts ON the event loop
    for (int i = 0; i < 100_000; i++) {
        processAccount(i); // blocks!
    }
});
JAVA
// CORRECT: delegate to a worker
vertx.setPeriodic(60_000, id -> {
    vertx.executeBlocking(promise -> {
        for (int i = 0; i < 100_000; i++) {
            processAccount(i); // safe on worker thread
        }
        promise.complete();
    });
});

How do you prevent duplicate execution in a cluster?

In a clustered deployment, every node receives the same timer tick, so you must prevent all nodes from running the same batch job. Use leader election or a distributed lock.

JAVA
package com.bank.batch;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.shareddata.Lock;

public class ClusterSafeBatch extends AbstractVerticle {
    @Override
    public void start() {
        vertx.setPeriodic(60_000, id -> {
            // Try to acquire a cluster-wide lock
            vertx.sharedData().getLockWithTimeout(
                    "batch.interest.lock", 1000, lockResult -> {
                if (lockResult.succeeded()) {
                    Lock lock = lockResult.result();
                    System.out.println("This node won the lock, running batch");
                    vertx.executeBlocking(promise -> {
                        runInterestBatch();
                        promise.complete();
                    }).onComplete(ar -> lock.release());
                } else {
                    System.out.println("Another node is running the batch");
                }
            });
        });
    }

    private void runInterestBatch() {
        System.out.println("Posting daily interest...");
    }
}

What about idempotency and chunking?

Long batch jobs should be idempotent and chunked so a crash mid-batch can be recovered safely.

JAVA
package com.bank.batch;

import java.math.BigDecimal;

public class ChunkedBatch {
    private static final int CHUNK_SIZE = 1000;

    public void processAllAccounts(int totalAccounts) {
        int offset = 0;
        while (offset < totalAccounts) {
            int end = Math.min(offset + CHUNK_SIZE, totalAccounts);
            processChunk(offset, end);
            // Commit per chunk so a crash loses at most one chunk
            System.out.println("Committed chunk " + offset + " to " + end);
            offset = end;
        }
    }

    private void processChunk(int start, int end) {
        BigDecimal rate = new BigDecimal("0.04");
        BigDecimal dailyRate = rate.divide(
                new BigDecimal("365"), 10, BigDecimal.ROUND_HALF_UP);
        for (int i = start; i < end; i++) {
            // Idempotent: re-running the same chunk produces the same result
            BigDecimal balance = new BigDecimal("100000.00");
            BigDecimal interest = balance.multiply(dailyRate);
        }
    }
}

What should a senior engineer know?

  • Size the worker pool deliberately with createSharedWorkerExecutor and monitor event-loop lag.
  • For very large data sets, prefer streaming and chunking over loading everything into memory.
  • Use the ordered parameter of executeBlocking to control whether multiple invocations run in parallel.
  • In production, monitor batch duration and set alerts when jobs exceed expected time.
JAVA
// Parallel execution: false means do NOT require ordered execution
vertx.executeBlocking(promise -> {
    processAllAccounts();
    promise.complete();
}, false); // multiple batches can run in parallel

Visual Overview

DIAGRAM
  Batch Job Flow in Vert.x:

  ┌───────────────┐   tick    ┌───────────────┐
  │ setPeriodic / │──────────▶│  Event Loop    │
  │ setTimer /    │           │  (handler)     │
  │ Quartz cron   │           │                │
  └───────────────┘           │  DO NOT block  │
                              │  here!         │
                              └───────┬────────┘
                                      │ delegate
                                      ▼
                              ┌───────────────┐
                              │ executeBlocking│
                              │ or Worker      │
                              │ Verticle       │
                              │                │
                              │ Heavy batch    │
                              │ work runs here │
                              └───────┬────────┘
                                      │ complete
                                      ▼
                              ┌───────────────┐
                              │ Callback on    │
                              │ Event Loop     │
                              │ (onSuccess)    │
                              └───────────────┘

  Cluster-Safe Execution:
  ┌──────┐  ┌──────┐  ┌──────┐
  │Node 1│  │Node 2│  │Node 3│
  └──┬───┘  └──┬───┘  └──┬───┘
     │ try     │ try     │ try
     │ lock    │ lock    │ lock
     ▼         ▼         ▼
  ┌──────────────────────────┐
  │  Distributed Lock        │
  │  (only one wins)         │
  └──────────────────────────┘
     │
     ▼
  Node 1 runs the batch
  Nodes 2, 3 skip

Key Takeaway

Yes, Vert.x runs batch and scheduled jobs well, as long as you respect the golden rule of never blocking the event loop. For simple fixed-rate scheduling, use setPeriodic, which fires a handler on the event loop at an interval and returns a timer id you can cancel. The crucial discipline is that the timer handler itself does almost nothing; its only job is to kick off the real work somewhere safe. The heavy batch processing goes onto worker threads, either through executeBlocking or inside a dedicated worker verticle, and for isolation create a sized shared worker executor so batch work never starves ordinary tasks.

For calendar-style cron scheduling, setPeriodic alone is not enough because it only does fixed intervals. Use the Vert.x Quartz extension or a cron parser to compute the delay until the next fire time, arm a timer for that delay, run the job on a worker, and re-arm for the following occurrence.

At National Bank of India Rajesh built exactly this for daily interest posting. A batch verticle schedules the trigger, and when it fires it submits the interest job to a dedicated worker pool via executeBlocking, so the event loop stays free to serve live balance and transfer requests. The job walks each account, computes the daily interest on the balance using BigDecimal, credits Rajesh and Priya, and returns the total. It is written to be idempotent and chunked so it can be retried safely after a crash. In production the job runs on a cron schedule, and because the deployment is a Vert.x cluster, a distributed lock ensures only the leader node executes the run so the bank never double-posts interest.

Remember one sentence: schedule the trigger on the event loop with setPeriodic or a cron timer, but always push the actual batch work onto a worker verticle or executeBlocking so the event loop stays free.

---

Share
Use ← → arrow keys to navigate