Skip to content

OxMQThe Virtual Thread-Native Distributed Queue for Java 17+ & 21

The modern Java alternative to BullMQ. Powered by Project Loom Virtual Threads, official BullMQ Lua scripts, native Context Isolation, and Request-Reply workflows.

OxMQ Logo

Quick Examples โ€‹

java
// 1. Initialize Queue & Worker
OxmqQueue<OrderPayload> queue = OxmqQueue.<OrderPayload>builder()
        .name("orders")
        .redisUri("redis://localhost:6379")
        .payloadClass(OrderPayload.class)
        .build();

OxmqWorker<OrderPayload> worker = OxmqWorker.<OrderPayload>builder()
        .queueName("orders")
        .redisUri("redis://localhost:6379")
        .concurrency(50) // 50 Virtual Threads on Java 21+
        .processor(job -> {
            job.updateProgress(50);
            return paymentGateway.charge(job.getData());
        })
        .build();
worker.start();

// 2. Enqueue with retry, backoff, and memory retention
Job<OrderPayload> job = queue.add("charge-card", 
        new OrderPayload("ord_101", 99.00),
        JobOptions.builder()
                .attempts(3)
                .exponentialBackoff(Duration.ofSeconds(1), Duration.ofSeconds(30))
                .removeOnComplete(KeepJobs.of(1000, Duration.ofHours(2)))
                .build());
java
// Cooperative AbortSignal parity on Java 21 Virtual Threads
OxmqWorker<VideoTask> worker = OxmqWorker.<VideoTask>builder()
        .queueName("video-rendering")
        .redisUri("redis://localhost:6379")
        .processor(job -> {
            // Listen to cooperative cancellation token
            job.getCancellationToken().onCancel(reason -> cleanupTempFiles(job.getId()));
            
            while (!job.isCancelled()) {
                renderNextFrame();
            }
            throw new JobCancelledException(job.getCancelReason());
        })
        .build();
worker.start();

// Externally cancel long-running job by ID
worker.cancelJob("video-job-42", "User clicked abort");
java
// Dequeue up to 100 jobs atomically with maxBatchBytes memory guardrail
OxmqBatchWorker<AuditRecord> batchWorker = OxmqBatchWorker.<AuditRecord>builder()
        .queueName("audit-stream")
        .redisUri("redis://localhost:6379")
        .batchSize(100)
        .maxBatchBytes(16 * 1024 * 1024) // 16 MB payload memory safeguard
        .batchTimeout(Duration.ofMillis(200))
        .processor(batch -> {
            List<AuditRecord> records = batch.stream().map(Job::getData).toList();
            clickHouseService.batchInsert(records); // Single bulk JDBC transaction!
            return "INSERTED_" + records.size();
        })
        .build();
batchWorker.start();
java
@Component
public class PaymentWorker {

    // Automatically runs on lightweight Virtual Threads in Java 21+
    @OxmqListener(queue = "orders", concurrency = 50, rateLimitMax = 100, rateLimitDurationMs = 1000)
    public PaymentResult handlePayment(Job<OrderPayload> job) {
        job.updateProgress(25);
        job.log("Charging credit card for order " + job.getData().orderId());
        return paymentGateway.charge(job.getData());
    }
}
java
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @Autowired
    private OxmqTemplate oxmqTemplate;

    @PostMapping("/checkout")
    public CompletableFuture<ResponseEntity<PaymentResult>> checkout(@RequestBody OrderPayload order) {
        Job<OrderPayload> job = oxmqTemplate.send("orders", "charge-card", order);
        
        // Reactively await completion via Redis stream events without blocking carrier threads
        return oxmqTemplate.waitUntilFinishedAsync("orders", job.getId(), Duration.ofSeconds(15))
                .thenApply(res -> ResponseEntity.ok((PaymentResult) res))
                .exceptionally(ex -> ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT).build());
    }
}
yaml
spring:
  application:
    name: order-service     # Auto-inherited as default OxMQ Redis namespace!

oxmq:
  redis:
    uri: redis://shared-redis-cluster:6379
  default-concurrency: 50
  virtual-threads: true     # Java 21 Loom (auto platform fallback on Java 17)
  metrics-enabled: true
  max-len-events: 10000    # Bounds Redis event stream
  default-job-options:
    remove-on-complete:
      count: 1000          # Keep latest 1,000 completed jobs
      age-seconds: 86400   # Evict completed jobs older than 24h
java
@Configuration
public class OxmqEnterprisePluginConfig {

    // 1. Dynamic Token Bucket per Tenant / Group Key
    @Bean
    public GroupRateLimiterPlugin groupRateLimiter(RedisConnectionManager conn, MeterRegistry meter) {
        return GroupRateLimiterPlugin.builder()
                .connectionManager(conn)
                .keyPrefix("oxmq:limiter:tenant")
                .groupKeyResolver(job -> ((TenantScopedPayload) job.getData()).tenantId())
                .dynamicLimit(tenantId -> licenseService.getBurstLimit(tenantId)) // e.g. 50-200 req/sec
                .windowDuration(Duration.ofSeconds(1))
                .retryDelayMs(150)
                .meterRegistry(meter)
                .build();
    }

    // 2. Upstream 429/503 Circuit Breaker Sentinel with Non-Destructive Delay
    @Bean
    public RateLimitSentinelPlugin rateLimitSentinel(RedisConnectionManager conn, MeterRegistry meter) {
        return RateLimitSentinelPlugin.builder()
                .connectionManager(conn)
                .keyPrefix("oxmq:sentinel:tenant")
                .groupKeyResolver(job -> ((TenantScopedPayload) job.getData()).tenantId())
                .errorMatcher(t -> t instanceof RateLimitCooldownException)
                .meterRegistry(meter)
                .build();
    }

    // 3. Priority Headroom Governor: Protect real-time jobs by yielding background sweeps
    @Bean
    public PriorityQuotaGovernorPlugin quotaGovernor(GroupRateLimiterPlugin limiter, MeterRegistry meter) {
        return PriorityQuotaGovernorPlugin.builder()
                .rateLimiter(limiter)
                .groupKeyResolver(job -> ((TenantScopedPayload) job.getData()).tenantId())
                .headroomYieldThreshold(0.25) // If < 25% token headroom remains
                .priorityThreshold(1)         // Strictly protect Priority <= 1 real-time jobs
                .yieldDelayMs(200)
                .meterRegistry(meter)
                .build();
    }
}

Slashing Operational Overhead in Enterprise Architectures โ€‹

OxMQ was purpose-built to eliminate the real-world operational bottlenecks and high memory costs that plague conventional Java queue architectures:

Conventional Java Queue BottleneckHow OxMQ Eliminates the OverheadReal-World Enterprise Impact
Heavy OS Platform Threads
Each worker thread consumes ~1 MB native stack. Under high I/O concurrency (HTTP calls, DB queries), thread pools exhaust memory and collapse.
Project Loom Virtual Threads
Each task executes on a lightweight virtual thread (~1 KB heap) that unmounts from carrier OS threads during I/O.
10,000+ concurrent workers on a single node without thread starvation or out-of-memory errors.
Redis Network Roundtrip Storms
Dequeuing 10,000 jobs individually triggers 10,000 network roundtrips, causing CPU spikes on Redis and microsecond latency accumulation.
Atomic Batch Dequeue (OxmqBatchWorker)
Bulk pops up to $N$ jobs in a single roundtrip via Lua script, paired with maxBatchBytes memory safety.
10x-50x reduction in network I/O, vaulting ingestion throughput past 3,500+ jobs/s for ClickHouse, Elasticsearch, and JDBC batching.
Redis Multi-Tenant Key Collisions
Microservices sharing a single Redis instance accidentally collide on common queue names (orders, notifications), cross-consuming jobs.
Native Context Isolation
Automatically prefixes keys as {service:queue} using the "Define Once, Inherit Everywhere" pattern (spring.application.name).
Zero data cross-talk and seamless multi-tenancy on shared Redis clusters without hardcoded prefix sprawl.
Upstream 429 Bans & Multi-Tenant Quota Starvation
Aggressive background workers exhaust external API quotas (Microsoft Graph, OpenAI, Stripe), triggering cluster-wide IP bans and starving real-time users.
Native Interceptors & Plugins SPI
Dynamic tenant token buckets (GroupRateLimiterPlugin), distributed 429 sentinels (RateLimitSentinelPlugin), and priority headroom yielding (PriorityQuotaGovernorPlugin) non-destructively delay jobs without burning retry attempts.
100% reliable third-party API integration with zero 429 unhandled failures, fair quota allocation, and zero upstream Lua modifications.
Redis Memory Creep & Unbounded Hash Growth
Completed and failed job records accumulate indefinitely until Redis hits maxmemory and begins rejecting writes.
Automated KeepJobs Retention
Atomic trimming by count, age, and batch limit built directly into completion Lua scripts, plus 8KB stack trace guardrails.
Deterministic, bounded Redis RAM footprint with automatic queue-level inheritance and zero manual maintenance cron jobs.
Redis Cluster CROSSSLOT Errors in DAGs
Parent-child workflow keys scattered across different hash slots crash Redis Cluster multi-key Lua commands.
Hash-Tag DAG Routing (FlowProducer.forCluster)
Wraps parent and all leaf child keys in a shared {clusterTag} CRC16 slot.
100% reliable multi-queue DAG execution on distributed Redis Cluster shards with zero cross-slot failures.

Released under the Apache 2.0 / MIT Open-Source Licenses.