In modern microservice stacks, one need shows up fast: background job queues. The moment an app has to send emails asynchronously, process files, generate PDFs, recompute reports, or run scheduled tasks without blocking HTTP requests, teams tend to reach for Redis, RabbitMQ, or Kafka — and with that, more infrastructure to operate.
Kool Queue takes the opposite approach: it’s an open-source utility for Micronaut that proposes something deliberately simple — use PostgreSQL as the queue backend, so you can avoid external dependencies. The project leans on a well-known database pattern: store jobs in a table and distribute them across multiple workers using non-blocking row locking via FOR UPDATE SKIP LOCKED.
What Kool Queue is — and why it’s interesting
Kool Queue positions itself as a DB-based queue backend for Micronaut, designed around simplicity and throughput, with one key technical decision: PostgreSQL + FOR UPDATE SKIP LOCKED so multiple workers can “pull” pending jobs without waiting on locks.
That’s a very real-world proposition: PostgreSQL is already in production, you want async execution, and you’d rather not introduce another moving part just to get a queue.
This isn’t “the universal queue for everything.” It’s a pragmatic fit for a specific class of problems — the same way Rails’ Solid Queue popularized the idea of database-backed jobs in another ecosystem.
The core mechanic: FOR UPDATE SKIP LOCKED
SKIP LOCKED means that if one worker has already locked a row (a job), other workers will ignore it and move on, instead of waiting. In the job-queue context, the common pattern is:
- Select a pending job
- Lock it (so no other worker can grab it)
- Mark it
IN_PROGRESS - Execute the work
- Mark it
DONEorERROR
Kool Queue describes this lifecycle explicitly, with states like PENDING → IN_PROGRESS → DONE/ERROR, and a scheduler that polls for work.
Installation and minimal dependencies
At the stack level, Kool Queue expects a typical Micronaut + JPA + PostgreSQL setup:
- Micronaut Data (Hibernate JPA)
- HikariCP
- PostgreSQL JDBC driver
Example (Kotlin/Gradle) starting point:
dependencies {
implementation("io.micronaut.data:micronaut-data-hibernate-jpa")
implementation("io.micronaut.sql:micronaut-jdbc-hikari")
runtimeOnly("org.postgresql:postgresql")
// Kool Queue (example; confirm the latest version you plan to use)
implementation("com.github.joaquindiez:micronaut-kool-queue:0.2.1")
}
Code language: JavaScript (javascript)
A typical application.yml (adapt as needed):
datasources:
default:
url: jdbc:postgresql://localhost:5432/your_db
username: your_username
password: your_password
driver-class-name: org.postgresql.Driver
jpa:
default:
properties:
hibernate:
hbm2ddl:
auto: update
micronaut:
scheduler:
kool-queue:
enabled: true
max-concurrent-tasks: 3
default-interval: 30s
default-initial-delay: 10s
shutdown-timeout-seconds: 30
Usage: define a job and enqueue it “later”
The library’s mental model is straightforward: you define a job class, implement process(...), and enqueue work via something like processLater(...).
Example (Kotlin):
import jakarta.inject.Singleton
data class EmailData(val recipient: String, val subject: String, val body: String)
@Singleton
class EmailNotificationJob : ApplicationJob<EmailData>() {
override fun process(data: EmailData): Result<Boolean> {
return try {
// Real logic: send an email, call an API, etc.
println("Sending email to ${data.recipient}: ${data.subject}")
Result.success(true)
} catch (e: Exception) {
Result.failure(e)
}
}
}
Code language: HTML, XML (xml)
From a controller:
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.*
@Controller("/notifications")
class NotificationController(private val emailJob: EmailNotificationJob) {
@Post("/send-email")
fun sendEmail(@Body emailData: EmailData): HttpResponse<String> {
val ref = emailJob.processLater(emailData)
return HttpResponse.ok("Email queued. Job ID: ${ref.jobId}")
}
}
Code language: HTML, XML (xml)
Operationally, the scheduler handles the rest: polling, honoring max concurrency, and updating status as jobs progress.
Where it fits — and where it doesn’t
A database-backed queue can be “more than enough” when the goal is to reduce operational complexity. But the boundaries matter.
| Approach | Extra infra | Best at | Typical risk | When it’s a good choice |
|---|---|---|---|---|
| Kool Queue (PostgreSQL) | No | Fewer components; fast adoption | Table growth; DB tuning; contention | Internal jobs, moderate throughput, minimalist stack |
| Redis + workers | Yes (Redis) | Very low latency; mature patterns | Running Redis + persistence concerns | High job volume, low latency needs, easy horizontal scaling |
| RabbitMQ | Yes | Routing, ACK/NACK, classic messaging | Operational + design overhead | Enterprise messaging and routed workflows |
| Kafka | Yes | Streaming, retention, replay | Complexity and cost | Event pipelines, auditing, data streaming |
| Solid Queue (Rails) | No | Proven patterns in Rails apps | Similar DB-queue limits | Rails apps avoiding Redis |
Sysadmin notes: how to keep it from becoming a problem later
If PostgreSQL is your queue, success is as much ops as it is code:
- Indexes matter: once the jobs table grows, you’ll want strong indexing on status/queue/scheduled time.
- Vacuum & bloat: status updates generate churn — watch autovacuum and table bloat.
- Transaction duration: long-running locks can still hurt throughput even with
SKIP LOCKED. - Observability: track backlog (
PENDING), error rates (ERROR), and time spentIN_PROGRESS. - Retries/backoff: define policies (max retries, delays, a “dead letter” equivalent if needed).
Kool Queue doesn’t pretend to erase these realities — it just offers a clean, direct path to “queues now,” using infrastructure you already run.
FAQs
How can I add background job processing in Micronaut without Redis?
By using a database-backed approach like Kool Queue, storing jobs in PostgreSQL and distributing work using FOR UPDATE SKIP LOCKED.
What are the main benefits of using PostgreSQL as a queue?
Less infrastructure to operate, simpler deployments, and fewer failure points — especially if PostgreSQL is already part of your production stack.
Does FOR UPDATE SKIP LOCKED help multiple workers run in parallel?
Yes. It lets each worker ignore rows already locked by others, reducing lock waits and improving concurrency under load.
When should I move from a DB queue to RabbitMQ/Kafka/Redis?
When you need very low latency at high volume, advanced routing semantics, retention/replay, strict workload isolation, or more specialized delivery guarantees.
