Definition
- 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.
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());
}
}
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.
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());
}
});
});
}
}
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.
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());
}
}
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.
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.
// 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!
}
});
// 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.
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.
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
createSharedWorkerExecutorand monitor event-loop lag. - For very large data sets, prefer streaming and chunking over loading everything into memory.
- Use the
orderedparameter ofexecuteBlockingto control whether multiple invocations run in parallel. - In production, monitor batch duration and set alerts when jobs exceed expected time.
// Parallel execution: false means do NOT require ordered execution
vertx.executeBlocking(promise -> {
processAllAccounts();
promise.complete();
}, false); // multiple batches can run in parallel
Visual Overview
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.
---