Concepts
Client, task, job, worker, result — the Java mental model over the Rust core.
Client, task, job, worker, result — the Java mental model over the Rust core.
The Java SDK is a typed shell; the scheduler, dispatcher, worker pool, and storage are all in the shared Rust engine. Five concepts cover the surface.
A typical JVM background-job setup runs several separate services: a message
broker (a standalone server that holds pending jobs — RabbitMQ, JMS, or a
Redis-backed queue) that hands work to consumers, somewhere to store results,
and the worker processes themselves. Spring's @Async skips the broker but
skips durability too — queued calls live in an in-process executor and vanish
on restart.
flexiq collapses all of that into the FlexiQ client: one SQLite file (or a
Postgres schema, or Redis) holds the job queue, the results, and any cron
schedules together. There's no broker to install, secure, or monitor —
FlexiQ.builder().sqlite(...).open() is the whole setup.
Your ConnectionFactory / spring.rabbitmq.* / JMS listener config
disappears — a single FlexiQ.builder().sqlite("flexiq.db").open()
replaces the broker, the result store, and the connection pool. Unlike
@Async, jobs survive an app restart: they're rows in storage, not
in-memory tasks.
A FlexiQ is the handle to one store (SQLite file, Postgres schema, or Redis
prefix), opened with FlexiQ.builder()...open(). You enqueue jobs, run
workers, and inspect state through it; it is AutoCloseable, so hold it in
try-with-resources. Multiple processes pointed at the same storage share one
logical queue. flexiq.queue(name) returns a handle to a single named queue
(pause / resume).
try (FlexiQ flexiq = FlexiQ.builder().sqlite("flexiq.db").open()) { ... }A task is a named unit of work described by a Task<T> — the name plus the
payload type it deserializes to. Enqueuing references the task by name, so the
producer never needs the handler — only the worker does. Per-task defaults
(queue, priority, retries, timeout) are attached fluently on the descriptor.
Enqueuing a task creates a job — a row in storage with payload, priority,
status, and result. Jobs move through a state machine: PENDING → RUNNING → COMPLETE, or FAILED (retrying), or DEAD (retries exhausted), or
CANCELLED. The engine claims jobs atomically, so the same job never runs
twice concurrently.
A failing job retries with backoff (each wait longer than the last) until its budget is exhausted, then lands in the dead-letter queue (DLQ, a holding area for exhausted jobs) — see dead-letter.
flexiq.worker() builds a worker: register handlers with .handle(task, fn),
then .start(). The Rust core polls storage, claims due jobs, and hands each
one to your handler on a thread pool; the result is serialized and written
back. close() stops dispatch and drains in-flight jobs.
flexiq.getResult(id, type) returns the handler's return value once the job
finishes, deserialized with the queue's
serializer. Results live in storage,
so any process sharing the store can read them; awaitJob(id, timeout) blocks
until the job is terminal.
The architecture is engine-level and shared across SDKs — see Architecture for the job lifecycle, scheduler, storage schema, and mesh internals.