= {
"resources",
],
},
+ java: {
+ id: "java",
+ label: "Java",
+ language: "Java",
+ binding: "JNI",
+ navSections: [
+ "java/getting-started",
+ "java/guides",
+ "architecture",
+ "java/api-reference",
+ "java/more/examples",
+ "resources",
+ ],
+ },
};
export function isSdk(value: string | null | undefined): value is Sdk {
diff --git a/docs/app/styles/docs.css b/docs/app/styles/docs.css
index ebe53c60..8bae1b3e 100644
--- a/docs/app/styles/docs.css
+++ b/docs/app/styles/docs.css
@@ -60,24 +60,75 @@ body {
padding: 0 5px;
font-size: 11px;
}
-/* SDK switcher (Python | Node) — sits above the nav tree */
-.sdk-switch {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 4px;
- padding: 4px;
- margin-bottom: 20px;
- background: var(--panel2);
+/* Topbar SDK dropdown (lives in .navright, before the search bar). */
+.sdk-dd {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ gap: 9px;
+}
+.sdk-dd-label {
+ font-family: var(--mono);
+ font-size: 11px;
+ letter-spacing: 0.05em;
+ color: var(--dim);
+ white-space: nowrap;
+}
+.sdk-dd-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
border: 1px solid var(--line2);
- border-radius: 11px;
+ border-radius: 10px;
+ background: var(--panel);
+ font-family: var(--mono);
+ font-size: 12.5px;
+ color: var(--txt2);
+ cursor: pointer;
+ transition: border-color 0.2s;
+}
+.sdk-dd-btn:hover {
+ border-color: var(--line3);
+}
+.sdk-dd-btn:focus-visible {
+ outline: 2px solid var(--indigo-br);
+ outline-offset: 1px;
+}
+.sdk-dd-icon {
+ display: inline-grid;
+ place-items: center;
+ width: 15px;
+ height: 15px;
+ color: var(--indigo-br);
+}
+.sdk-dd-icon svg {
+ width: 15px;
+ height: 15px;
+}
+.sdk-dd .sdk-caret {
+ color: var(--mut);
}
-.sdk-opt {
+.sdk-dd-menu {
+ position: absolute;
+ top: calc(100% + 6px);
+ right: 0;
+ z-index: 60;
+ min-width: 150px;
+ padding: 5px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ background: var(--panel);
+ border: 1px solid var(--line2);
+ border-radius: 12px;
+ box-shadow: var(--shadow-md);
+}
+.sdk-dd-opt {
display: flex;
align-items: center;
- justify-content: center;
- gap: 7px;
- width: 100%;
- padding: 8px 6px;
+ gap: 9px;
+ padding: 8px 10px;
border: 0;
border-radius: 8px;
background: transparent;
@@ -85,27 +136,29 @@ body {
font-size: 13px;
font-weight: 500;
color: var(--mut);
- text-decoration: none;
+ text-align: left;
cursor: pointer;
transition:
background 0.15s,
- color 0.15s,
- box-shadow 0.15s;
-}
-.sdk-opt svg {
- width: 15px;
- height: 15px;
- flex: none;
+ color 0.15s;
}
-.sdk-opt:hover {
+.sdk-dd-opt:hover {
+ background: var(--panel2);
color: var(--txt);
}
-.sdk-opt.active {
+.sdk-dd-opt.active {
color: var(--indigo-br);
- background: var(--panel);
- box-shadow:
- 0 1px 3px rgba(0, 0, 0, 0.12),
- 0 0 0 1px var(--indigo-line);
+ background: var(--indigo-soft);
+}
+.sdk-dd-check {
+ margin-left: auto;
+ color: var(--indigo-br);
+}
+/* The label is site chrome — hide it before the nav collapses, keep the button. */
+@media (max-width: 1080px) {
+ .sdk-dd-label {
+ display: none;
+ }
}
.nav-group {
margin-bottom: 22px;
@@ -472,6 +525,8 @@ body {
flex: none;
width: 22px;
height: 22px;
+ /* Optically center the icon on the first text line. */
+ margin-top: 1px;
color: var(--indigo-br);
}
.callout .cc {
@@ -479,6 +534,18 @@ body {
line-height: 1.6;
color: var(--txt2);
}
+/* MDX wraps callout text in ; prose margins/size would push it off the
+ icon line and override the callout's tighter type scale. */
+.callout .cc p {
+ font-size: inherit;
+ line-height: inherit;
+}
+.callout .cc > p:first-child {
+ margin-top: 0;
+}
+.callout .cc > p:last-child {
+ margin-bottom: 0;
+}
.callout .cc strong {
color: var(--txt);
font-weight: 600;
diff --git a/docs/content/docs/architecture/resources.mdx b/docs/content/docs/architecture/resources.mdx
index 7f3fa310..978bed9d 100644
--- a/docs/content/docs/architecture/resources.mdx
+++ b/docs/content/docs/architecture/resources.mdx
@@ -48,3 +48,23 @@ the worker before the handler runs. Recipes are optionally HMAC-signed for
tamper detection.
+
+
+
+**Layer 1 — Registration**: resources are registered once with
+`queue.resource(name, factory)` (plus scope and pool-config overloads). A
+factory may depend on another resource through the context it receives
+(`ctx.use("other")`), and a cycle fails fast instead of recursing.
+
+**Layer 2 — Worker Resource Runtime**: each worker leases a `ResourceRuntime`
+that builds resources on first use and disposes them LIFO at shutdown. Handlers
+reach them with `Resources.use("name")` (or generated `@Resource` bindings); the
+scope — worker / thread / task / request / pooled — decides each resource's
+lifetime, with pooled instances checked out per task and returned at task end.
+
+**Layer 3 — Resource Proxies**: `ProxyHandler` implementations deconstruct live
+objects (file handles and similar) into a serializable reference and
+reconstruct them before use. References are HMAC-SHA256-signed, with optional
+expiry and purpose binding folded into the signature.
+
+
diff --git a/docs/content/docs/architecture/serialization.mdx b/docs/content/docs/architecture/serialization.mdx
index 65d09b38..bbea27e8 100644
--- a/docs/content/docs/architecture/serialization.mdx
+++ b/docs/content/docs/architecture/serialization.mdx
@@ -4,8 +4,8 @@ description: "Pluggable serializers for task arguments and results, with a per-S
---
taskito uses a pluggable serializer for task arguments and results. The default
-is CloudpickleSerializer} node={JsonSerializer} />,
-which .
+is CloudpickleSerializer} node={JsonSerializer} java={JsonSerializer} />,
+which .
@@ -29,6 +29,21 @@ new Queue({ dbPath: "taskito.db", serializer: new MsgpackSerializer() });
+
+
+```java
+import org.byteveda.taskito.Taskito;
+import org.byteveda.taskito.serialization.MsgpackSerializer;
+
+// Use msgpack for compact, binary payloads
+Taskito queue = Taskito.builder()
+ .sqlite("taskito.db")
+ .serializer(new MsgpackSerializer())
+ .open();
+```
+
+
+
## Built-in serializers
@@ -51,9 +66,20 @@ new Queue({ dbPath: "taskito.db", serializer: new MsgpackSerializer() });
+
+
+| Serializer | Format | Best for |
+|---|---|---|
+| `JsonSerializer` (default) | JSON (Jackson) | Simple types, cross-language interop, debugging |
+| `MsgpackSerializer` | Binary (msgpack) | Compact payloads, smaller storage |
+| `SignedSerializer` | Wraps a serializer + HMAC-SHA256 | Tamper detection |
+| `EncryptedSerializer` | Wraps a serializer + AES-GCM | Encrypted, authenticated payloads |
+
+
+
## Custom serializers
-Implement the `Serializer` interface with dumps(obj) / loads(data)>} node={<>serialize(value) / deserialize(bytes)>} /> methods.
+Implement the `Serializer` interface with dumps(obj) / loads(data)>} node={<>serialize(value) / deserialize(bytes)>} java={<>serialize(value) / deserialize(bytes, type)>} /> methods.
## What gets serialized
diff --git a/docs/content/docs/architecture/worker-pool.mdx b/docs/content/docs/architecture/worker-pool.mdx
index 15399066..96a6721c 100644
--- a/docs/content/docs/architecture/worker-pool.mdx
+++ b/docs/content/docs/architecture/worker-pool.mdx
@@ -43,3 +43,17 @@ functions.
between concurrently dispatched jobs.
+
+
+
+- **JVM executor threads**: handlers run on a `java.util.concurrent`
+ `ExecutorService` owned by the worker — a cached pool by default, a fixed pool
+ with `concurrency(n)`, or a resizable pool under `autoscale(...)`.
+- **Native dispatch bridge**: the Rust core hands each claimed job to the JVM
+ through a JNI dispatch bridge; the handler's return value (or exception) is
+ bridged back into the Rust scheduler as the job outcome.
+- **Context isolation**: each invocation gets its own `TaskScope` for resource
+ resolution, entered and exited around the handler on its executor thread —
+ thread-confined, so concurrent jobs never share per-task state.
+
+
diff --git a/docs/content/docs/java/api-reference/batching.mdx b/docs/content/docs/java/api-reference/batching.mdx
new file mode 100644
index 00000000..443f0c35
--- /dev/null
+++ b/docs/content/docs/java/api-reference/batching.mdx
@@ -0,0 +1,91 @@
+---
+title: Batching
+description: "Batcher — buffer payloads and flush them as one enqueueMany call."
+---
+
+```java
+import org.byteveda.taskito.batch.Batcher;
+```
+
+`Batcher` buffers payloads for one task and enqueues them in a single
+`enqueueMany` call once the buffer reaches a size limit or a delay elapses —
+producer-side batching, to cut round-trips when many small jobs share a task.
+
+
+ This is a different concept from `Worker.Builder.batchSize(int)`. `Batcher`
+ batches *what a producer enqueues*; `Worker.Builder.batchSize` controls *how
+ many jobs a worker claims per scheduler poll* (dequeue batching). They share
+ a name only — see [Worker](/java/api-reference/worker#options).
+
+
+## Constructor
+
+```java
+Batcher(Taskito queue, Task task, int maxBatch, Duration maxDelay)
+static Batcher of(Taskito queue, Task task, int maxBatch, Duration maxDelay)
+```
+
+| Parameter | Description |
+|---|---|
+| `queue` | The client to enqueue through. |
+| `task` | The task every buffered payload is enqueued against. |
+| `maxBatch` | Flush once the buffer reaches this many payloads. Must be > 0. |
+| `maxDelay` | Flush this long after the first buffered payload arrived, even if `maxBatch` isn't reached. Must be positive. |
+
+```java
+try (Batcher batcher = Batcher.of(taskito, sendEmail, 100, Duration.ofMillis(500))) {
+ for (Email email : emails) {
+ batcher.add(email);
+ }
+}
+```
+
+## `add`
+
+```java
+List add(T payload)
+```
+
+Buffers `payload`. Returns the flushed job ids if this call pushed the buffer
+to `maxBatch` (triggering an immediate flush), otherwise an empty list — the
+delayed flush is still pending.
+
+## `flush`
+
+```java
+List flush()
+```
+
+Enqueues whatever is currently buffered right now, cancelling any pending
+delayed flush. Returns the new job ids, or an empty list if the buffer was
+empty.
+
+## `close`
+
+```java
+void close()
+```
+
+`AutoCloseable`. Flushes any remaining buffered payloads, then stops the
+batcher's background scheduler. Use try-with-resources so nothing buffered is
+lost when the producer shuts down.
+
+`Batcher` is backed by a single-thread daemon `ScheduledExecutorService` and
+guards its buffer with an internal lock — `add`/`flush`/`close` are safe to
+call from multiple threads.
+
+## Underlying producer calls
+
+`Batcher` is a thin wrapper over the batch producer methods on
+[`Taskito`](/java/api-reference/queue):
+
+```java
+ List enqueueMany(Task task, List payloads)
+ List enqueueMany(Task task, List payloads, EnqueueOptions options)
+ List enqueueAll(Task task, List payloads) // alias of enqueueMany
+```
+
+All three enqueue the full list in one storage call and return job ids in
+input order (no dedup). A single `EnqueueOptions` applies to every job in the
+batch — there is no per-job options list, so a batch can't mix, say,
+different priorities or delays across its items in one call.
diff --git a/docs/content/docs/java/api-reference/canvas.mdx b/docs/content/docs/java/api-reference/canvas.mdx
new file mode 100644
index 00000000..365cf97d
--- /dev/null
+++ b/docs/content/docs/java/api-reference/canvas.mdx
@@ -0,0 +1,84 @@
+---
+title: Canvas
+description: "Compact signature reference for Canvas.link, chain, group, and chord."
+---
+
+```java
+import org.byteveda.taskito.workflows.Canvas;
+```
+
+Shortcuts that build a [`Workflow`](/java/api-reference/workflows) from a few
+`Canvas.link` building blocks — sequential, parallel, or a parallel group
+joined by a callback. For worked examples and diagrams, see the
+[Canvas guide](/java/guides/workflows/canvas).
+
+## `Canvas.link`
+
+```java
+static Link link(String name, Task task, T payload)
+```
+
+A named step bound to a task and payload. `Link` is otherwise opaque — pass
+it straight into `chain`, `group`, or `chord`.
+
+## `Canvas.chain`
+
+```java
+static Workflow chain(String name, Link... links)
+```
+
+Runs `links` one after another — each link's step depends on the previous
+one.
+
+```java
+Workflow etl = Canvas.chain("etl",
+ Canvas.link("extract", extractTask, source),
+ Canvas.link("transform", transformTask, null),
+ Canvas.link("load", loadTask, null));
+
+taskito.submitWorkflow(etl);
+```
+
+## `Canvas.group`
+
+```java
+static Workflow group(String name, Link... links)
+```
+
+Runs `links` in parallel — no dependencies between them.
+
+```java
+Workflow notify = Canvas.group("notify",
+ Canvas.link("email", sendEmail, message),
+ Canvas.link("sms", sendSms, message));
+
+taskito.submitWorkflow(notify);
+```
+
+## `Canvas.chord`
+
+```java
+static Workflow chord(String name, Link callback, Link... group)
+```
+
+Runs `group` in parallel, then `callback` once every member of `group`
+completes. The callback depends on every group link's step; it runs with its
+own configured payload, not the group's aggregated results.
+
+```java
+Workflow report = Canvas.chord("report",
+ Canvas.link("merge", mergeTask, null), // callback
+ Canvas.link("q1", queryRegion, "east"),
+ Canvas.link("q2", queryRegion, "west"));
+
+taskito.submitWorkflow(report);
+```
+
+## Submitting
+
+Every `Canvas` constructor returns a full `Workflow` — there's no separate
+canvas-specific submit path. Submit it like any other workflow via
+[`Taskito.submitWorkflow`](/java/api-reference/queue), and build the worker
+with `.trackWorkflows()` so the run's state and node statuses are tracked.
+`chain`/`group`/`chord` steps are plain task nodes, so no deferred-node
+registration (`trackWorkflows(workflow)`) is required for them.
diff --git a/docs/content/docs/java/api-reference/cli.mdx b/docs/content/docs/java/api-reference/cli.mdx
new file mode 100644
index 00000000..874cd4f7
--- /dev/null
+++ b/docs/content/docs/java/api-reference/cli.mdx
@@ -0,0 +1,37 @@
+---
+title: CLI
+description: "The taskito command."
+---
+
+```bash
+taskito [--backend ] [--url ] [args]
+```
+
+`org.byteveda.taskito.cli.Cli` is a picocli command (`Cli.main`) over a
+`Taskito` client. `--backend` defaults to `sqlite`; `--url` is the connection
+string (a SQLite path, defaulting to `.taskito/taskito.db`, or a
+Postgres/Redis URL — required for those backends). Read commands print JSON.
+
+## Commands
+
+| Command | Description |
+|---|---|
+| `stats` | Job counts by status. |
+| `enqueue ['']` | Enqueue a job (payload defaults to null) → job id. |
+| `jobs [--status ] [--queue ] [--limit ]` | List jobs. |
+| `cancel ` | Cancel a pending job (exit 1 if it already started). |
+| `pause ` / `resume ` | Queue control. |
+| `dlq list [--limit ]` / `dlq retry ` / `dlq delete ` | Dead-letter ops. |
+| `dashboard [--port ] [--token ] [--static ]` | Serve the dashboard until interrupted. |
+
+```bash
+taskito --url taskito.db enqueue add '{"a":2,"b":3}'
+taskito --url taskito.db jobs --status failed --limit 20
+taskito --backend postgres --url postgres://localhost/taskito stats
+```
+
+Run it from your application's classpath, e.g.:
+
+```bash
+java -cp app.jar org.byteveda.taskito.cli.Cli --url taskito.db stats
+```
diff --git a/docs/content/docs/java/api-reference/context.mdx b/docs/content/docs/java/api-reference/context.mdx
new file mode 100644
index 00000000..e304f4e6
--- /dev/null
+++ b/docs/content/docs/java/api-reference/context.mdx
@@ -0,0 +1,61 @@
+---
+title: Context
+description: "Resources.use inside handlers and the middleware TaskContext."
+---
+
+A handler's first argument is always its deserialized payload — a lambda
+handler takes nothing else, while a `@TaskHandler` method may declare further
+`@Resource(...)` parameters that the generated companion injects. The
+execution context around it is reached two ways: resource resolution inside
+the handler, and the `TaskContext` handed to middleware.
+
+## `Resources.use`
+
+```java
+import org.byteveda.taskito.resources.Resources;
+
+taskito.worker().handle(sendEmail, payload -> {
+ SmtpClient smtp = Resources.use("smtp");
+ return smtp.deliver(payload);
+});
+```
+
+Resolves a [registered resource](/java/api-reference/resources) by name against
+the current task's scope. Valid only while a task runs on this worker — the
+worker binds the scope around the handler call; outside one it throws a
+`ResourceException`. Prefer the declarative `@Resource("smtp")` parameter on a
+`@TaskHandler` method, which the generated companion resolves the same way.
+
+## `TaskContext` (middleware)
+
+The job's identity travels through [middleware](/java/api-reference/queue)
+hooks rather than the handler signature:
+
+```java
+taskito.use(new Middleware() {
+ @Override
+ public void before(TaskContext context) {
+ MDC.put("jobId", context.jobId);
+ }
+
+ @Override
+ public void onError(TaskContext context, Throwable error) {
+ log.warn("{} {} failed", context.taskName, context.jobId, error);
+ }
+});
+```
+
+| Member | Description |
+|---|---|
+| `jobId` / `taskName` | The executing job's identity. |
+| `attributes()` | Mutable per-execution scratch map shared across `before`/`after`/`onError` for the same job (e.g. a timer or a span). |
+| `job()` | The executing job, including its lazily loaded metadata. |
+
+## Progress & cancellation
+
+Progress and cooperative cancellation go through the client by job id:
+`taskito.setProgress(jobId, percent)` records 0–100 for inspection and the
+dashboard, and `taskito.isCancelRequested(jobId)` lets a long-running handler
+poll for a `requestCancel` from another process. Thread the job id into the
+payload (or capture it in middleware `attributes()`) when a handler needs
+either.
diff --git a/docs/content/docs/java/api-reference/errors.mdx b/docs/content/docs/java/api-reference/errors.mdx
new file mode 100644
index 00000000..e4ce7b90
--- /dev/null
+++ b/docs/content/docs/java/api-reference/errors.mdx
@@ -0,0 +1,40 @@
+---
+title: Errors
+description: "The TaskitoException hierarchy — catch by specific type or by the base."
+---
+
+Every error the SDK throws extends `TaskitoException` (an unchecked
+`RuntimeException`), so a single `catch` can scope all of them; the subclasses
+under `org.byteveda.taskito.errors` let you branch on what went wrong.
+
+```java
+try {
+ taskito.enqueue(sendPromo, audience);
+} catch (PredicateRejectedException rejected) {
+ respond(409, "blocked by policy");
+} catch (TaskitoException other) {
+ report(other);
+}
+```
+
+## Hierarchy
+
+| Class | Extends | Thrown when |
+|---|---|---|
+| `TaskitoException` | `RuntimeException` | Base; also carries native (JNI) errors directly. |
+| `ConfigurationException` | `TaskitoException` | The SDK is misconfigured — e.g. a missing connection URL. |
+| `SerializationException` | `TaskitoException` | A payload, options blob, or native response can't be (de)serialized. |
+| `CryptoException` | `SerializationException` | A signing/encrypting serializer or codec failed — bad signature, short payload, cipher error. |
+| `PredicateRejectedException` | `TaskitoException` | A registered predicate rejected an enqueue; no job was created. |
+| `EnqueueSkippedException` | `TaskitoException` | A gate returned `Skip` — `enqueue` throws; use `tryEnqueue` to get an empty `Optional` instead. |
+| `InterceptionException` | `TaskitoException` | An interceptor rejected an enqueue. |
+| `LockException` | `TaskitoException` | A distributed-lock operation failed or was interrupted. |
+| `ResourceException` | `TaskitoException` | A worker resource couldn't be built, resolved, or disposed — including `Resources.use` outside a task. |
+| `ProxyException` | `TaskitoException` | A proxy reference couldn't be created or reconstructed — no handler, signature mismatch, allowlist violation. |
+| `WebhookException` | `TaskitoException` | A webhook couldn't be stored, loaded, signed, or encoded. |
+| `WorkflowException` | `TaskitoException` | Workflow definition, submission, await-timeout, or query error. |
+
+Handler exceptions are different: an exception thrown *inside* a
+`TaskFunction` isn't rethrown to you — it fails that attempt, and the core
+retries with the task's backoff until the retry budget is spent, then
+dead-letters the job. Inspect those via `jobErrors(id)` and `listDead`.
diff --git a/docs/content/docs/java/api-reference/index.mdx b/docs/content/docs/java/api-reference/index.mdx
new file mode 100644
index 00000000..3435f7e5
--- /dev/null
+++ b/docs/content/docs/java/api-reference/index.mdx
@@ -0,0 +1,22 @@
+---
+title: API Reference
+description: "The Java SDK public surface — the Taskito client, tasks, workers, workflows, and the CLI."
+---
+
+The public API lives under `org.byteveda.taskito` (Maven coordinates
+`org.byteveda:taskito`). Contrib helpers live under
+`org.byteveda.taskito.contrib`; the Spring Boot 3 starter ships separately as
+`org.byteveda:taskito-spring`.
+
+| Page | Covers |
+|---|---|
+| [Taskito](/java/api-reference/queue) | `Taskito.builder()`, enqueue, inspect, admin, locks, periodic. |
+| [Task](/java/api-reference/task) | `Task.of` typed tasks, `EnqueueOptions`, `RetryPolicy`, the annotations. |
+| [Worker](/java/api-reference/worker) | The `Worker.Builder` and the running worker handle. |
+| [Result](/java/api-reference/result) | Awaiting jobs and reading typed results. |
+| [Context](/java/api-reference/context) | `Resources.use` and the middleware `TaskContext`. |
+| [Resources](/java/api-reference/resources) | Worker dependency injection — scopes, pools, disposal. |
+| [Serializers](/java/api-reference/serializers) | `JsonSerializer`, `MsgpackSerializer`, payload codecs. |
+| [Workflows](/java/api-reference/workflows) | The `Workflow` builder, `WorkflowRun`, gates, sagas, analysis. |
+| [Errors](/java/api-reference/errors) | The `TaskitoException` hierarchy. |
+| [CLI](/java/api-reference/cli) | The `taskito` command. |
diff --git a/docs/content/docs/java/api-reference/meta.json b/docs/content/docs/java/api-reference/meta.json
new file mode 100644
index 00000000..371cf33a
--- /dev/null
+++ b/docs/content/docs/java/api-reference/meta.json
@@ -0,0 +1,22 @@
+{
+ "title": "API Reference",
+ "root": true,
+ "pages": [
+ "index",
+ "overview",
+ "queue",
+ "task",
+ "worker",
+ "result",
+ "context",
+ "resources",
+ "serializers",
+ "workflows",
+ "canvas",
+ "saga",
+ "batching",
+ "testing",
+ "errors",
+ "cli"
+ ]
+}
\ No newline at end of file
diff --git a/docs/content/docs/java/api-reference/overview.mdx b/docs/content/docs/java/api-reference/overview.mdx
new file mode 100644
index 00000000..948ca947
--- /dev/null
+++ b/docs/content/docs/java/api-reference/overview.mdx
@@ -0,0 +1,90 @@
+---
+title: Overview
+description: "The layered architecture behind the client — from the public Taskito interface down to the native core."
+---
+
+```java
+import org.byteveda.taskito.Taskito;
+
+try (Taskito taskito = Taskito.builder().sqlite("taskito.db").open()) {
+ // taskito is the client — every operation in this reference goes through it
+}
+```
+
+The public API is a thin, typed shell over the Rust core. App code only ever
+touches the first layer below; the rest exist so the SDK stays testable and
+the native boundary stays narrow.
+
+## Layers
+
+| Layer | Package | Role |
+|---|---|---|
+| `Taskito` | `org.byteveda.taskito` | The public entry point. An `AutoCloseable` interface built via `Taskito.builder()...open()`. Every method documented in this reference is declared here. |
+| `DefaultTaskito` | `org.byteveda.taskito` (package-private) | The only implementation of `Taskito`. Owns predicates, gates, interceptors, resources, and middleware; delegates producer/inspection/admin calls to a `QueueBackend` and job-await polling to `CoreFacade`. |
+| `CoreFacade` | `org.byteveda.taskito.core` | Translation and durable-emulation layer between the typed API and the backend's JSON wire views — e.g. `awaitJob` polls `getJobJson` until the job reaches a terminal status. |
+| `QueueBackend` | `org.byteveda.taskito.spi` | The SPI every backend implements. App code depends only on this interface; it's also what a test fake targets (see [Testing](/java/api-reference/testing)). |
+| `JniQueueBackend` | `org.byteveda.taskito.internal` (package-private) | The production `QueueBackend` — calls into the native shell over JNI. All scheduling, storage, and locking lives in the Rust core; this layer only marshals JSON and bytes across the boundary. |
+
+A call like `taskito.enqueue(task, payload)` flows `Taskito` (interface) →
+`DefaultTaskito` (serializes the payload, runs gates/interceptors/middleware)
+→ `QueueBackend` (`JniQueueBackend` in production) → the native core.
+
+## `Queue` is a handle, not the client
+
+`Taskito.queue(name)` returns a `Queue` — a handle to one *named queue*, not
+the client itself:
+
+```java
+public interface Queue {
+ String name();
+ void pause();
+ void resume();
+ boolean isPaused();
+}
+```
+
+
+ Everything else — enqueue, inspect, workers, workflows, locks, periodics —
+ lives on `Taskito`. Reach for `Queue` only to pause or resume one named
+ queue; see [Taskito](/java/api-reference/queue).
+
+
+## Packages
+
+Every package lives under `org.byteveda.taskito`:
+
+| Package | Contents |
+|---|---|
+| *(root)* | `Taskito`, `Queue`, `NamedQueue`, `TaskitoException` |
+| `task` | `Task`, `EnqueueOptions`, `RetryPolicy` |
+| `annotation` | `@TaskHandler`, `@Resource`, `@Compressed`, `@Encrypted` |
+| `worker` | `Worker`, `Worker.Builder`, `Handler`, `HandlerRegistry` |
+| `workflows` | `Workflow`, `Step`, `Canvas`, `WorkflowRun`, gates, sagas, analysis |
+| `resources` | Worker dependency injection — scopes, pools, `ResourceContext` |
+| `serialization` | `Serializer`, `JsonSerializer`, `MsgpackSerializer`, payload codecs |
+| `locks` | `Lock`, `LockInfo` |
+| `predicates` | Enqueue-time gates — `Predicate`, `EnqueueGate`, `Recipes` |
+| `interception` | Enqueue interceptors — `Interceptor`, `Interception` |
+| `proxies` | Non-serializable argument proxies — `ProxyHandler`, `ProxySession` |
+| `middleware` | `Middleware`, `EnqueueContext`, `TaskContext` |
+| `events` | `EventName`, `OutcomeEvent` |
+| `errors` | The `TaskitoException` hierarchy |
+| `model` | Wire-view records — `Job`, `QueueStats`, `WorkerInfo`, … |
+| `scheduling` | `PeriodicTask` |
+| `autoscale` | `AutoscaleOptions`, `Autoscaler` |
+| `batch` | `Batcher` |
+| `webhooks` | `Webhook`, `WebhookManager` |
+| `contrib` | Optional integrations (Micrometer, Sentry) |
+| `dashboard` | Dashboard asset serving |
+| `cli` | The `taskito` command |
+| `spi` | `QueueBackend` — the interface every backend implements |
+| `core` | `CoreFacade` — translation and durable emulation |
+| `internal` | `JniQueueBackend` and the JNI transport (package-private) |
+
+## Maven coordinates
+
+| Artifact | Contents |
+|---|---|
+| `org.byteveda:taskito` | The SDK itself. |
+| `org.byteveda:taskito-test` | `InMemoryTaskito` / `InMemoryQueueBackend` — see [Testing](/java/api-reference/testing). |
+| `org.byteveda:taskito-spring` | The Spring Boot 3 starter. |
diff --git a/docs/content/docs/java/api-reference/queue.mdx b/docs/content/docs/java/api-reference/queue.mdx
new file mode 100644
index 00000000..e81848eb
--- /dev/null
+++ b/docs/content/docs/java/api-reference/queue.mdx
@@ -0,0 +1,102 @@
+---
+title: Taskito
+description: "Construct the client and the full producer/admin API."
+---
+
+```java
+import org.byteveda.taskito.Taskito;
+
+try (Taskito taskito = Taskito.builder().sqlite("taskito.db").open()) {
+ // ...
+}
+```
+
+`Taskito` is `AutoCloseable`; `close()` releases the native storage handle.
+
+## Builder
+
+| Method | Description |
+|---|---|
+| `backend(String)` | `"sqlite"` (default) \| `"postgres"` \| `"redis"`. |
+| `url(String)` | Connection string: a file path for SQLite, a URL for Postgres/Redis. |
+| `sqlite()` / `sqlite(path)` / `postgres(url)` / `redis(url)` | Backend shortcuts. SQLite defaults to `.taskito/taskito.db`. |
+| `poolSize(int)` | Connection pool size (SQLite/Postgres). |
+| `schema(String)` | Postgres schema (default `taskito`). |
+| `prefix(String)` | Redis key prefix. |
+| `namespace(String)` | Namespace applied to enqueued jobs + the worker scheduler. |
+| `serializer(Serializer)` | Payload/result serializer (default `JsonSerializer`). |
+| `codec(PayloadCodec...)` | Global [codec chain](/java/api-reference/serializers) around the serializer. |
+| `codec(String, PayloadCodec)` | Register a named codec for per-task selection (`Task.codecs`). |
+| `open()` | Open the native backend → `Taskito`. |
+| `open(QueueBackend)` | Open over an explicit backend (e.g. a test fake). |
+
+## Producer
+
+| Method | Description |
+|---|---|
+| `enqueue(Task, T)` / `enqueue(Task, T, EnqueueOptions)` | Enqueue a typed payload → job id. |
+| `enqueue(String taskName, Object payload)` | Enqueue by task name. |
+| `tryEnqueue(...)` | Gate-aware `enqueue`: `Optional.empty()` when a gate skips; a `Reject` still throws. |
+| `enqueueMany(Task, List)` / `(..., EnqueueOptions)` | Batch-enqueue in one storage call → ids in input order (no dedup). |
+| `enqueueAll(Task, List)` | Alias of `enqueueMany`. |
+
+## Results & cancellation
+
+| Method | Description |
+|---|---|
+| `getJob(id)` → `Optional` | A job snapshot (status, progress, timestamps). |
+| `awaitJob(id, Duration)` → `Optional` | Block until terminal; throws on timeout. |
+| `getResult(id)` → `Optional` / `getResult(id, Class)` → `Optional` | The job's result, raw or [deserialized](/java/api-reference/result). |
+| `cancel(id)` | Cancel a pending job. |
+| `requestCancel(id)` / `isCancelRequested(id)` | Cooperative cancellation of a running job. |
+| `setProgress(id, int)` | Record 0–100 progress for inspection / the dashboard. |
+
+## Inspection
+
+| Method | Description |
+|---|---|
+| `stats()` / `statsByQueue(q)` / `statsAllQueues()` | `QueueStats` counts by status. |
+| `listJobs(JobFilter)` | List jobs — filter by status/queue/task with limit/offset. |
+| `jobErrors(id)` → `List` | Per-attempt error history. |
+| `metrics(taskName, sinceMs)` → `List` | Per-execution metrics (null task = all). |
+| `listWorkers()` → `List` | Registered workers (heartbeat + identity). |
+| `writeTaskLog(...)` / `getTaskLogs(id)` | Structured task-log lines for a job. |
+
+## Admin
+
+| Method | Description |
+|---|---|
+| `listDead(limit, offset)` / `listDeadByTask(task, limit, offset)` | Dead-letter entries. |
+| `retryDead(deadId)` (alias `retry`) / `deleteDead(deadId)` | Re-enqueue / drop an entry. |
+| `purgeDead(olderThanMs)` / `purgeDeadByTask(task)` | Bulk DLQ cleanup → count removed. |
+| `purgeCompleted(olderThanMs)` | Drop old completed jobs. |
+| `queue(name)` → `Queue` | A named-queue handle: `name()`, `pause()`, `resume()`, `isPaused()`. |
+| `listPausedQueues()` | Names of every paused queue. |
+| `getSetting` / `setSetting` / `deleteSetting` / `listSettings` | Shared key–value settings in the store. |
+
+## Locks & periodic
+
+| Method | Description |
+|---|---|
+| `lock(name, ttlMs)` / `lock(name)` | A distributed `Lock` (default 30s TTL): `acquire()`, `tryAcquire(timeout)`, `extend(ttlMs)`, `close()`. |
+| `withLock(name, ttlMs, Runnable)` | Acquire → run → release; returns whether it ran. |
+| `lockInfo(name)` (alias `getLockInfo`) | Current holder metadata, if held. |
+| `registerPeriodic(PeriodicTask)` | Register (or replace) a cron task → next fire time (Unix ms). |
+| `listPeriodic()` / `deletePeriodic(name)` / `pausePeriodic(name)` / `resumePeriodic(name)` | Periodic task management. |
+
+## Subsystems
+
+| Method | Description |
+|---|---|
+| `worker()` → `Worker.Builder` | Begin building a [worker](/java/api-reference/worker). |
+| `use(Middleware)` | Cross-cutting hooks — `onEnqueue`, `before`/`after`/`onError`, and the outcome hooks. |
+| `resource(...)` | Register an injectable [resource](/java/api-reference/resources). |
+| `predicate(taskName, Predicate)` / `gate(taskName, EnqueueGate)` | Enqueue-time gates: reject, skip, or defer submissions. |
+| `intercept(Interceptor)` | Pass / convert / redirect / reject each enqueue before serialization. |
+| `submitWorkflow(Workflow)` / `submitWorkflow(Workflow, Map)` | Submit a [workflow](/java/api-reference/workflows) → `WorkflowRun`. |
+| `workflowStatus(runId)` / `cancelWorkflow(runId)` | Run queries and cancellation. |
+| `resourceMetrics()` | Per-resource created/disposed/active counters. |
+
+`WebhookManager.attach(taskito)` registers outcome webhooks, and
+`Scaler.start(taskito, ScalerOptions.onPort(9090))` serves a queue-depth
+endpoint for external autoscalers — both build on this client.
diff --git a/docs/content/docs/java/api-reference/resources.mdx b/docs/content/docs/java/api-reference/resources.mdx
new file mode 100644
index 00000000..9e6014b1
--- /dev/null
+++ b/docs/content/docs/java/api-reference/resources.mdx
@@ -0,0 +1,67 @@
+---
+title: Resources
+description: "Worker dependency injection — scopes, pools, and disposal."
+---
+
+Resources are worker-side dependencies (clients, connections, caches) built by
+a factory and handed to handlers by name — via `Resources.use("name")` or an
+`@Resource("name")` parameter.
+
+## Registration
+
+```java
+Taskito taskito = Taskito.builder().sqlite("app.db").open();
+
+taskito.resource("smtp", context -> SmtpClient.connect(config));
+taskito.resource("tx", ResourceScope.TASK,
+ context -> context.use("db").begin(),
+ tx -> tx.rollbackIfOpen());
+taskito.resource("db", PoolConfig.of(10).withMaxLifetime(Duration.ofMinutes(30)),
+ context -> Database.connect(dsn),
+ Database::close);
+```
+
+| Overload | Description |
+|---|---|
+| `resource(name, factory)` | Worker-scoped (the default). |
+| `resource(name, scope, factory)` | Explicit `ResourceScope`. |
+| `resource(name, scope, factory, dispose)` | With a disposer run when the scope ends. |
+| `resource(name, PoolConfig, factory, dispose)` | A `POOLED` resource: a bounded pool checked out per task. |
+
+The factory receives a `ResourceContext` — `scope()` plus `use(name)` to depend
+on other resources. A factory may only depend on same-or-longer-lived
+resources (a `WORKER` factory can't use a `TASK` resource), and a `POOLED`
+factory may only use `WORKER` resources — pooled instances outlive any one
+task, so a shorter-lived dependency would dangle after its scope ends.
+
+## Scopes
+
+| `ResourceScope` | Lifetime |
+|---|---|
+| `WORKER` | Built once, lazily; shared across every task; disposed at worker teardown. |
+| `THREAD` | Built lazily once per worker thread; disposed at worker shutdown. |
+| `TASK` | Built lazily per task invocation; disposed when the task ends. |
+| `REQUEST` | Built fresh on every `use()`; disposed when the task ends — never cached. |
+| `POOLED` | A bounded pool; each task checks out one instance and returns it at task end. |
+
+Task-scoped disposal runs in reverse build order (LIFO), so dependents tear
+down before their dependencies.
+
+## `PoolConfig`
+
+`PoolConfig.of(poolSize)` plus `withPoolMin(n)` (eager prewarm at worker
+start), `withAcquireTimeout(Duration)` (checkout wait limit, default 10s), and
+`withMaxLifetime(Duration)` (idle instances older than this are disposed
+instead of reused). Part of the cross-SDK contract for pooled resources.
+
+## Metrics
+
+`taskito.resourceMetrics()` returns a `Map` of per-resource
+counters — `created`, `disposed`, and `active` (`created − disposed`).
+
+
+ Registration happens on the `Taskito` client, but resources are built lazily
+ on the worker — a producer-only process never constructs them. See
+ [Context](/java/api-reference/context) for resolving resources inside a
+ handler.
+
diff --git a/docs/content/docs/java/api-reference/result.mdx b/docs/content/docs/java/api-reference/result.mdx
new file mode 100644
index 00000000..8aa69f71
--- /dev/null
+++ b/docs/content/docs/java/api-reference/result.mdx
@@ -0,0 +1,52 @@
+---
+title: Result
+description: "Awaiting jobs and reading typed results."
+---
+
+```java
+String id = taskito.enqueue(add, payload);
+
+taskito.awaitJob(id, Duration.ofSeconds(30)); // block until terminal
+Optional sum = taskito.getResult(id, Integer.class); // 5
+```
+
+## Awaiting
+
+`awaitJob(id, timeout)` polls the store until the job reaches a terminal state
+(`COMPLETE`, `DEAD`, or `CANCELLED`) and returns its `Optional` snapshot;
+it throws a `TaskitoException` if the timeout elapses first. Any process
+sharing the storage can await a job by id.
+
+## Reading the result
+
+| Method | Description |
+|---|---|
+| `getResult(id)` → `Optional` | The raw serialized result, if complete. |
+| `getResult(id, Class)` → `Optional` | The result deserialized with the queue's [serializer](/java/api-reference/serializers). |
+
+Both return `Optional.empty()` while the job is still pending or running — pair
+them with `awaitJob` (or an `EventName.SUCCESS` listener on the worker) when
+you need to block.
+
+## The `Job` snapshot
+
+`getJob(id)` returns the job's current state without waiting:
+
+```java
+taskito.getJob(id).ifPresent(job -> {
+ System.out.println(job.status); // JobStatus.RUNNING
+ System.out.println(job.progress); // 0–100, if reported
+ System.out.println(job.error); // last error, if any
+});
+```
+
+`Job` carries `id`, `queue`, `taskName`, `status`, `priority`, `createdAt`,
+`scheduledAt`, `startedAt`, `completedAt`, `retryCount`, `maxRetries`,
+`timeoutMs`, `progress`, `error`, `uniqueKey`, `namespace`, and `metadata`.
+All timestamps are Unix milliseconds.
+
+
+ `awaitJob` is for waiting on one job (tests, request/response bridges). To
+ scan many jobs use `listJobs(JobFilter)`; for aggregate outcomes use
+ `metrics(taskName, sinceMs)`.
+
diff --git a/docs/content/docs/java/api-reference/saga.mdx b/docs/content/docs/java/api-reference/saga.mdx
new file mode 100644
index 00000000..9dfe3def
--- /dev/null
+++ b/docs/content/docs/java/api-reference/saga.mdx
@@ -0,0 +1,103 @@
+---
+title: Saga
+description: "Step.Builder.compensate, the saga run/node states, and the rollback contract."
+---
+
+Saga compensation is declared per step, at build time — there is no
+separate saga type or task-level default compensator. For the conceptual
+walkthrough and a full example, see the [Sagas guide](/java/guides/workflows/saga).
+
+## `Step.Builder.compensate`
+
+```java
+Step.Builder compensate(String compensateTask)
+Step.Builder compensate(Task> compensateTask)
+```
+
+Registers a rollback task for the step. If the run later fails, completed
+compensable steps roll back in reverse-dependency order, each compensation
+job receiving the step's own forward result as its payload.
+
+```java
+Workflow checkout = Workflow.named("checkout")
+ .step(Step.of("reserve", reserve, orderId).compensate(unreserve).build())
+ .step(Step.of("charge", charge, orderId).after("reserve").compensate(refund).build())
+ .step(Step.of("ship", ship, orderId).after("charge").build());
+```
+
+## Validation
+
+`Step.build()` rejects a compensator on nodes that have no forward result to
+replay:
+
+| Step kind | `compensate` allowed |
+|---|---|
+| Plain task step | Yes |
+| Fan-in step | Yes — the collected list is a real forward result |
+| Fan-out step | No — `IllegalArgumentException` at `build()` |
+| Gate | No — `IllegalArgumentException` at `build()` |
+| Sub-workflow | No — `IllegalArgumentException` at `build()` |
+
+## Compensator functions
+
+A compensator is a plain task handler — no special interface. Register it
+like any other task and give its payload type the forward task's result type:
+
+```java
+record Charge(String chargeId, String orderId) {}
+
+Task charge = Task.of("chargePayment", String.class);
+Task refund = Task.of("refundPayment", Charge.class);
+
+worker.handle(charge, orderId -> new Charge(stripe.charge(orderId).id(), orderId));
+worker.handle(refund, result -> { stripe.refund(result.chargeId()); return null; });
+```
+
+Each compensation job inherits the forward step's `queue`, `maxRetries`,
+`timeoutMs`, and `priority` — it retries and dead-letters like any other job.
+
+## Run states
+
+`WorkflowState` gains three saga states, all counted by `isTerminal()` except
+`COMPENSATING`:
+
+| State | Terminal | Meaning |
+|---|:---:|---|
+| `COMPENSATING` | No | The run failed and rollback jobs are in flight. |
+| `COMPENSATED` | Yes | All compensators for the run completed successfully. |
+| `COMPENSATION_FAILED` | Yes | At least one compensator itself failed; remaining un-rolled-back steps are left as-is. |
+
+`WorkflowRun.await(...)` unblocks on any terminal state, including these two.
+
+## Node states
+
+`NodeStatus` gains the parallel per-node states:
+
+| Status | Meaning |
+|---|---|
+| `COMPENSATING` | A compensation job has been enqueued for this node. |
+| `COMPENSATED` | The node's compensation job succeeded. |
+| `COMPENSATION_FAILED` | The node's compensation job exhausted its retries. |
+
+Only nodes that actually ran their forward task this run (`COMPLETED`) are
+compensable — `CACHE_HIT`, `FAILED`, `SKIPPED`, and pending nodes never had a
+side effect here to undo, so they're left alone.
+
+## Rollback mechanics
+
+When a run with compensable steps fails:
+
+1. Completed compensable steps are grouped into reverse-dependency waves —
+ deepest (most downstream) nodes first.
+2. Each wave's compensation jobs are enqueued in parallel; the next wave only
+ dispatches once the current one fully drains.
+3. If every wave drains without a failure, the run ends `COMPENSATED`. If any
+ compensation job fails, rollback stops there (fail-stop) and the run ends
+ `COMPENSATION_FAILED` — later waves are never dispatched.
+
+Every compensation job is enqueued with a deterministic unique key,
+`compensation:{runId}:{nodeName}`, and `compensation: true` metadata
+alongside `workflow_run_id` / `workflow_node_name` — so a tracker restart
+mid-rollback re-enqueues idempotently instead of duplicating the job, and a
+compensation job's own outcome is routed back into the saga rather than the
+forward run.
diff --git a/docs/content/docs/java/api-reference/serializers.mdx b/docs/content/docs/java/api-reference/serializers.mdx
new file mode 100644
index 00000000..62525c1f
--- /dev/null
+++ b/docs/content/docs/java/api-reference/serializers.mdx
@@ -0,0 +1,73 @@
+---
+title: Serializers
+description: "JsonSerializer, MsgpackSerializer, payload codecs, and the Serializer interface."
+---
+
+```java
+import org.byteveda.taskito.serialization.JsonSerializer;
+import org.byteveda.taskito.serialization.MsgpackSerializer;
+
+Taskito taskito = Taskito.builder()
+ .sqlite("taskito.db")
+ .serializer(new MsgpackSerializer())
+ .open();
+```
+
+## Built-in serializers
+
+| Class | Description |
+|---|---|
+| `JsonSerializer` | Default. JSON via Jackson; accepts a custom `ObjectMapper`. |
+| `MsgpackSerializer` | Compact binary (MessagePack via Jackson). Its `org.msgpack:jackson-dataformat-msgpack` dependency is `compileOnly` — add it to your build. |
+| `SignedSerializer(delegate, key)` | Prefixes each payload with an HMAC-SHA256 tag; verifies it (constant-time) on read — authentication, not encryption. |
+| `EncryptedSerializer(delegate, key)` | AES-GCM with a fresh 12-byte IV per payload — confidentiality **and** integrity. Key must be 16/24/32 bytes. |
+
+Producers and workers must configure a compatible serializer — the Rust core
+treats payloads as opaque bytes.
+
+## Payload codecs
+
+A `PayloadCodec` is a byte-to-byte transform (`encode`/`decode`) applied after
+serialization and reversed before deserialization — one implementation owns
+both directions so the inverse can't drift. Codecs follow the cross-SDK
+contract, so payloads survive producer/worker boundaries between SDKs.
+
+| Codec | Description |
+|---|---|
+| `GzipCodec` | Compression; decode is capped (default 64 MiB) against zip bombs. |
+| `AesGcmCodec(key)` | AES-GCM encryption, `[12-byte IV][ciphertext‖tag]`. |
+| `HmacCodec(key)` | HMAC-SHA256 signing, `[32-byte MAC][body]`, constant-time verify. |
+
+A chain applies in order on encode and in reverse on decode — order `GzipCodec`
+*before* a signing/encryption codec so integrity is verified before
+decompressing:
+
+```java
+Taskito taskito = Taskito.builder()
+ .sqlite("taskito.db")
+ .codec(new GzipCodec(), new HmacCodec(secret)) // global chain
+ .codec("encrypted", new AesGcmCodec(key)) // named, per-task
+ .open();
+
+Task buildReport = Task.of("build_report", Report.class)
+ .codecs("encrypted"); // this task only
+```
+
+The global chain wraps the queue serializer (a `CodecSerializer` under the
+hood) and covers results too. Named codecs apply per task via `Task.codecs`
+or the `@Compressed`/`@Encrypted` annotations — the same names (and keys) must
+be registered on producers and workers.
+
+## The `Serializer` interface
+
+```java
+public interface Serializer {
+ byte[] serialize(Object value);
+ T deserialize(byte[] bytes, Class type);
+ default Object deserialize(byte[] bytes, Type type) { ... }
+}
+```
+
+The `Type` overload supports generic payloads from a `TypeReference`; the
+default implementation handles plain `Class` types and rejects generic ones —
+override it in a generics-aware implementation (as `JsonSerializer` does).
diff --git a/docs/content/docs/java/api-reference/task.mdx b/docs/content/docs/java/api-reference/task.mdx
new file mode 100644
index 00000000..b4129b61
--- /dev/null
+++ b/docs/content/docs/java/api-reference/task.mdx
@@ -0,0 +1,95 @@
+---
+title: Task
+description: "Task.of typed tasks, EnqueueOptions, RetryPolicy, and the annotations."
+---
+
+```java
+import org.byteveda.taskito.task.Task;
+
+Task sendEmail = Task.of("send_email", EmailPayload.class);
+```
+
+A `Task` is an immutable descriptor: a name, the payload type, and default
+enqueue options. The fluent option methods each return a new descriptor.
+
+## Factories
+
+| Method | Description |
+|---|---|
+| `Task.of(name, Class)` | Payload deserializes to `payloadType`. |
+| `Task.of(name, TypeReference)` | Generic payloads a `Class` token can't express, e.g. `new TypeReference>(){}`. |
+
+## Fluent options
+
+| Method | Description |
+|---|---|
+| `queue(String)` | Target queue (default `default`). |
+| `priority(int)` | Higher runs first. |
+| `maxRetries(int)` (alias `retries`) | Attempts before dead-lettering. |
+| `timeoutMs(long)` / `timeout(Duration)` | Per-attempt timeout. |
+| `delayMs(long)` / `delay(Duration)` | Schedule after a delay. |
+| `retryPolicy(RetryPolicy)` | Backoff curve — registered with the worker on `start()`. |
+| `codecs(String...)` | Named [payload codecs](/java/api-reference/serializers) applied to this task. |
+| `withOptions(EnqueueOptions)` | Replace the default options wholesale. |
+
+```java
+Task sendEmail = Task.of("send_email", EmailPayload.class)
+ .queue("emails")
+ .maxRetries(5)
+ .timeout(Duration.ofSeconds(30))
+ .retryPolicy(RetryPolicy.exponential(Duration.ofSeconds(2), Duration.ofMinutes(5)));
+```
+
+## `EnqueueOptions`
+
+Per-enqueue overrides, built with `EnqueueOptions.builder()`; unset fields take
+core defaults. Fields: `queue`, `priority`, `maxRetries`, `timeoutMs` /
+`timeout(Duration)`, `delayMs` / `delay(Duration)`, `uniqueKey` (alias
+`jobId`) for idempotent enqueues, `metadata`, `namespace`.
+
+```java
+taskito.enqueue(sendEmail, payload, EnqueueOptions.builder()
+ .delay(Duration.ofHours(24))
+ .uniqueKey("welcome:" + userId)
+ .build());
+```
+
+## `RetryPolicy`
+
+| Factory | Description |
+|---|---|
+| `exponential(base, max)` | Retry N waits about `base · 2^N`, capped at `max`, plus jitter. |
+| `delays(Duration...)` | Explicit per-attempt delays, applied exactly (no jitter). |
+
+The retry *budget* comes from `maxRetries`; the policy only supplies the
+timing, which the core scheduler applies durably.
+
+## Annotations
+
+`@TaskHandler` marks a method as a task handler. A compile-time processor
+(`org.byteveda:taskito` ships it as the `processor` artifact) generates a
+`FooTasks` companion per enclosing class `Foo` — a typed `Task` constant per
+handler plus `bind(Worker.Builder, Foo)` and `handlers(Foo)` — no runtime
+reflection.
+
+```java
+class Mailer {
+ @TaskHandler("send_email")
+ Receipt send(EmailPayload payload, @Resource("smtp") SmtpClient smtp) {
+ return smtp.deliver(payload);
+ }
+}
+
+Worker worker = taskito.worker()
+ .register(MailerTasks.handlers(new Mailer()))
+ .start();
+```
+
+| Annotation | Description |
+|---|---|
+| `@TaskHandler(value, queue, maxRetries, timeoutMs, priority)` | Task name defaults to the method name; other fields default to the core defaults. |
+| `@Resource("name")` | Injects a worker [resource](/java/api-reference/resources) into a parameter after the payload. |
+| `@Compressed` | Records the `"compressed"` codec on the generated task. |
+| `@Encrypted` | Records the `"encrypted"` codec — register the actual codec (and key) at runtime. |
+
+All are source-retention: read at compile time, never at runtime.
diff --git a/docs/content/docs/java/api-reference/testing.mdx b/docs/content/docs/java/api-reference/testing.mdx
new file mode 100644
index 00000000..c2da8e42
--- /dev/null
+++ b/docs/content/docs/java/api-reference/testing.mdx
@@ -0,0 +1,89 @@
+---
+title: Testing
+description: "InMemoryTaskito and InMemoryQueueBackend — a pure-Java backend for fast unit tests."
+---
+
+```java
+import org.byteveda.taskito.test.InMemoryTaskito;
+```
+
+Ships in the `org.byteveda:taskito-test` artifact (the `:test-support`
+subproject). The testing model is: swap in an in-memory backend, run a real
+in-process worker against it, and synchronize on results the same way you
+would against a native backend. For a walkthrough with assertions, see the
+[Testing guide](/java/guides/operations/testing).
+
+```kotlin
+testImplementation("org.byteveda:taskito-test:0.18.0")
+```
+
+## `InMemoryTaskito`
+
+```java
+static Taskito open()
+static Taskito open(Serializer serializer)
+```
+
+Opens a [`Taskito`](/java/api-reference/queue) over a fresh
+`InMemoryQueueBackend`. `open()` uses the default JSON serializer; `open(Serializer)`
+swaps it. To share one backend instance across a custom builder, construct it
+explicitly:
+
+```java
+Taskito taskito = Taskito.builder().open(new InMemoryQueueBackend());
+```
+
+## `InMemoryQueueBackend`
+
+A pure-Java `QueueBackend` implementation — no JNI, no disk. It provides the
+full producer, inspection, admin, locks, and log surface, plus a small
+in-process polling worker: dispatch → complete/fail/cancel → retry or
+dead-letter, honoring per-worker queue filters and priority/FIFO ordering
+among pending jobs.
+
+```java
+try (Taskito taskito = InMemoryTaskito.open()) {
+ String id = taskito.enqueue("add", List.of(2, 3));
+
+ try (Worker worker = taskito.worker()
+ .handle("add", List.class, numbers -> (int) numbers.get(0) + (int) numbers.get(1))
+ .start()) {
+ taskito.awaitJob(id, Duration.ofSeconds(5));
+ }
+
+ assertEquals(5, taskito.getResult(id, Integer.class).orElseThrow());
+}
+```
+
+## Limitations
+
+| Capability | Behavior |
+|---|---|
+| Workflows | **Unsupported.** Every workflow `QueueBackend` method (`submitWorkflow`, `expandFanOut`, `getWorkflowStatusJson`, …) throws `UnsupportedOperationException`. Use a native backend (e.g. a throwaway SQLite file) for workflow tests. |
+| Periodic tasks | `registerPeriodic` and friends are recorded in a catalog — `listPeriodic`, `deletePeriodic`, `pausePeriodic` all work against it — but registered periodics never fire. |
+
+## Faking dependencies
+
+There is no dedicated mock-resource type — register a stub factory the same
+way you'd register a real one:
+
+```java
+taskito.resource("db", context -> fakeDb);
+```
+
+The worker injects whatever is registered, so no real connection is opened
+during the test.
+
+## Synchronization
+
+`awaitJob(id, timeout)` blocks until the job reaches a terminal state and is
+the simplest synchronization point. An event listener with a `CountDownLatch`
+works too:
+
+```java
+CountDownLatch done = new CountDownLatch(1);
+Worker worker = taskito.worker()
+ .handle("echo", String.class, payload -> payload.length())
+ .on(EventName.SUCCESS, event -> done.countDown())
+ .start();
+```
diff --git a/docs/content/docs/java/api-reference/worker.mdx b/docs/content/docs/java/api-reference/worker.mdx
new file mode 100644
index 00000000..8dc9bea5
--- /dev/null
+++ b/docs/content/docs/java/api-reference/worker.mdx
@@ -0,0 +1,70 @@
+---
+title: Worker
+description: "The Worker.Builder and the running worker handle."
+---
+
+```java
+Worker worker = taskito.worker()
+ .handle(sendEmail, payload -> mailer.deliver(payload))
+ .queues("default", "emails")
+ .start();
+```
+
+`taskito.worker()` returns a `Worker.Builder`; `start()` launches the native
+scheduler and returns a running `Worker` (an `AutoCloseable`).
+
+## `Worker.Builder`
+
+### Handlers
+
+| Method | Description |
+|---|---|
+| `handle(Task, TaskFunction)` | Register a typed handler; the task's retry policy and codecs come with it. |
+| `handle(String taskName, Class, TaskFunction)` | Register by name + payload class. |
+| `register(Handler, ?>)` | A `Handler.of(task, function)` pair. |
+| `register(HandlerRegistry)` | Every handler in a bundle (e.g. a generated `FooTasks.handlers(impl)`). |
+| `apply(Consumer)` | Apply a customizer (e.g. a generated `FooTasks.bind`). |
+
+`TaskFunction` is `R apply(T payload) throws Exception` — the return
+value becomes the job result; a thrown exception fails the attempt.
+
+### Options
+
+| Method | Default | Description |
+|---|---|---|
+| `queues(String...)` | `default` | Queue names to serve. |
+| `concurrency(int)` | 0 (cached pool) | Fixed handler-thread count. |
+| `channelCapacity(int)` | 128 | In-flight dispatch channel capacity. |
+| `batchSize(int)` | 1 | Jobs claimed per scheduler poll. |
+| `autoscale(AutoscaleOptions)` | — | Resize the handler pool between `min..max` threads by queue depth. |
+| `mesh(MeshOptions)` | — | Join a scheduling mesh (gossip discovery + work stealing). |
+| `on(EventName, Consumer)` | — | Outcome listeners: `SUCCESS`, `RETRY`, `DEAD`, `CANCELLED`. |
+| `trackWorkflows()` / `trackWorkflows(Workflow...)` | off | Drive [workflow](/java/api-reference/workflows) state from this worker's outcomes; register workflows whose deferred nodes need payloads. |
+
+`AutoscaleOptions.of(min, max)` defaults to ~10 tasks per worker, re-evaluated
+every 2s. `MeshOptions.builder().port(7946).seed("10.0.0.2:7946").build()` is
+all most clusters set — the database stays the source of truth, so mesh is a
+pure throughput optimization.
+
+## `Worker`
+
+| Method | Description |
+|---|---|
+| `stop()` | Stop dispatching; in-flight jobs continue to drain. |
+| `close()` | Stop, drain in-flight handlers (30s grace), free the native worker, tear down worker resources. Idempotent. |
+| `awaitShutdown()` | Block until `close()` is called. |
+| `approveGate(runId, node)` / `rejectGate(runId, node, reason)` | Resolve a parked workflow gate (requires `trackWorkflows`). |
+| `meshClusterInfo()` | `Optional` — peer count, capacity, load (mesh workers only). |
+
+```java
+try (Worker worker = taskito.worker()
+ .handle(sendEmail, mailer::deliver)
+ .concurrency(8)
+ .on(EventName.DEAD, event -> log.warn("dead: {}", event.jobId))
+ .start()) {
+ worker.awaitShutdown();
+}
+```
+
+The worker registers itself and heartbeats so it appears on the dashboard
+Workers panel; `close()` unregisters it.
diff --git a/docs/content/docs/java/api-reference/workflows.mdx b/docs/content/docs/java/api-reference/workflows.mdx
new file mode 100644
index 00000000..8181ffde
--- /dev/null
+++ b/docs/content/docs/java/api-reference/workflows.mdx
@@ -0,0 +1,100 @@
+---
+title: Workflows
+description: "The Workflow builder, WorkflowRun handle, gates, sagas, and analysis."
+---
+
+```java
+Workflow etl = Workflow.named("etl")
+ .step("extract", extract, source)
+ .step("transform", transform, null, "extract")
+ .step("load", load, null, "transform");
+
+WorkflowRun run = taskito.submitWorkflow(etl);
+WorkflowStatus done = run.await(Duration.ofMinutes(5));
+```
+
+Steps run in topological order; a step waits for every predecessor named in
+its `after` list. The running worker must be built with `trackWorkflows()`
+(and `trackWorkflows(workflow)` for gates, sub-workflows, callable conditions,
+and other deferred nodes).
+
+## `Workflow` builder
+
+| Method | Description |
+|---|---|
+| `Workflow.named(name)` / `version(int)` | Start a definition (version defaults to 1). |
+| `step(name, task, payload, after...)` | A task node with a baked-in payload. |
+| `stepAfter(name, task, deps...)` | A structural node — payload supplied at submit via `submitWorkflow(wf, payloads)`. |
+| `step(Step)` | A fully-configured `Step` (see below). |
+| `fanOut(name, task, FanMode.EACH, after)` | One child per item of the predecessor's list result. |
+| `fanIn(name, task, FanMode.ALL, after)` | Collect the fan-out children's results into one list. |
+| `gate(name, GateConfig, after...)` | Park for approval — a control node, never enqueued. |
+| `subWorkflow(name, child, after...)` | Submit `child` as a child run; completes when it finalizes. |
+
+`Canvas` adds shortcuts: `Canvas.chain(name, links...)` (sequential),
+`Canvas.group(name, links...)` (parallel), and `Canvas.chord(name, callback,
+group...)` (parallel group joined by a callback), where each link is
+`Canvas.link(name, task, payload)`.
+
+## `Step.of(...)` builder
+
+`Step.of(name, task, payload)` (or `(name, task)` for runtime-derived
+payloads) then: `after(String...)`, `queue`, `maxRetries`, `timeoutMs`,
+`priority`, `fanOut`/`fanIn(FanMode)`, `gate(GateConfig)`,
+`condition("on_success" | "on_failure" | "always")` — with `onSuccess()` /
+`onFailure()` / `always()` shorthands — `condition(Condition)` (a code
+predicate over the run's `WorkflowContext`), `compensate(task)` (saga
+rollback), and `cache(Duration ttl)` (skip re-running an unchanged step;
+requires a predecessor). `build()` validates the combination.
+
+## Gates
+
+`GateConfig.manual()` waits indefinitely; `GateConfig.timeout(duration,
+GateAction.APPROVE | GateAction.REJECT, message)` auto-resolves after the
+timeout. Resolve a parked gate from the tracking worker:
+
+```java
+worker.approveGate(run.id(), "review");
+worker.rejectGate(run.id(), "review", "failed QA");
+```
+
+## Sagas
+
+A step with `compensate(rollbackTask)` registers a rollback: when the run
+fails, completed compensable steps roll back in reverse-dependency order, each
+compensation job receiving the step's forward result as its payload. The run
+ends `COMPENSATED`, or `COMPENSATION_FAILED` if a rollback itself fails.
+
+## `WorkflowRun`
+
+| Member | Description |
+|---|---|
+| `id()` (alias `runId()`) / `name()` | The run's identity. |
+| `status()` | `Optional` — the current run + node snapshot. |
+| `await(timeout)` / `await(timeout, pollInterval)` | Block until terminal; throws `WorkflowException` on timeout. |
+| `cancel()` | Skip pending nodes and mark the run cancelled. |
+
+`WorkflowStatus` exposes `state` (a `WorkflowState`: `RUNNING`, `COMPLETED`,
+`COMPLETED_WITH_FAILURES`, `FAILED`, `CANCELLED`, `COMPENSATING`,
+`COMPENSATED`, `COMPENSATION_FAILED`, …), `nodes` (per-step `NodeSnapshot`
+with `status`, `jobId`, `fanOutCount`, timestamps), `isTerminal()`,
+`node(name)`, and `failedStep()`. `taskito.workflowStatus(runId)` and
+`taskito.cancelWorkflow(runId)` work from any process by run id.
+
+## Analysis & visualization
+
+`WorkflowAnalysis` is static graph computation over a *definition* — use it to
+validate or preview structure before submitting:
+
+```java
+WorkflowAnalysis.validate(etl); // throws on cycles / unknown deps
+WorkflowAnalysis.topologicalOrder(etl); // a valid run order
+WorkflowAnalysis.levels(etl); // nodes grouped by dependency depth
+WorkflowAnalysis.roots(etl); // entry nodes
+WorkflowAnalysis.leaves(etl); // exit nodes
+WorkflowAnalysis.ancestors(etl, "load"); // transitive upstream deps
+WorkflowAnalysis.descendants(etl, "extract");
+```
+
+`WorkflowVisualization.mermaid(workflow)` and `.dot(workflow)` render the DAG
+for docs or dashboards.
diff --git a/docs/content/docs/java/getting-started/capabilities.mdx b/docs/content/docs/java/getting-started/capabilities.mdx
new file mode 100644
index 00000000..78849af6
--- /dev/null
+++ b/docs/content/docs/java/getting-started/capabilities.mdx
@@ -0,0 +1,69 @@
+---
+title: Capabilities at a glance
+description: "Everything the Java SDK can do, with a link to each guide."
+---
+
+A map of the Java SDK's features. Everything here runs over the shared Rust
+core — pick a backend, describe tasks, run workers.
+
+## Core
+
+| Feature | Guide |
+|---|---|
+| Typed task descriptors, enqueue, read results | [Tasks](/java/guides/core/tasks) |
+| SQLite / Postgres / Redis backends | [Installation](/java/getting-started/installation#pick-a-backend) |
+| Named queues, priority, pause/resume | [Queues](/java/guides/core/queues) |
+| Cron-scheduled & delayed jobs | [Scheduling](/java/guides/core/scheduling) |
+| Cooperative cancellation & progress | [Cancellation](/java/guides/core/cancellation) |
+| Partial results via task logs | [Streaming](/java/guides/core/streaming) |
+| Execution model (thread pools, autoscaling) | [Execution model](/java/guides/core/execution-model) |
+| Compile-time `TaskHandler` bindings (no reflection) | [Tasks](/java/guides/core/tasks#typed-tasks-from-taskhandler) |
+
+## Reliability
+
+| Feature | Guide |
+|---|---|
+| Retries with backoff curves (`RetryPolicy`), dead-letter queue | [Retries](/java/guides/reliability/retries) · [Dead-letter](/java/guides/reliability/dead-letter) |
+| Per-attempt timeouts | [Timeouts](/java/guides/reliability/timeouts) |
+| Idempotent enqueue (`uniqueKey`) | [Idempotency](/java/guides/reliability/idempotency) |
+| Distributed locks | [Locks](/java/guides/reliability/locks) |
+| At-least-once delivery | [Guarantees](/java/guides/reliability/guarantees) |
+
+## Orchestration
+
+| Feature | Guide |
+|---|---|
+| DAG workflows: fan-out/in, conditions, gates, sub-workflows, sagas, visualization | [Workflows](/java/guides/workflows) |
+| Decentralized work-stealing mesh | [Mesh](/java/guides/operations/mesh) |
+
+## Extensibility
+
+| Feature | Guide |
+|---|---|
+| Lifecycle events & middleware | [Events](/java/guides/extensibility/events) · [Middleware](/java/guides/extensibility/middleware) |
+| Signed webhook delivery | [Webhooks](/java/guides/extensibility/webhooks) |
+| Resource injection, enqueue predicates & interception | [Resources](/java/guides/resources) · [Predicates](/java/guides/core/predicates) |
+| Signed cross-process references (proxies) | [Resources](/java/guides/resources) |
+| Pluggable serializers (JSON / MessagePack / signed / encrypted) & payload codecs | [Serializers](/java/guides/extensibility/serializers) |
+| Producer-side batching (`Batcher`) | [Extensibility](/java/guides/extensibility) |
+
+## Observability & ops
+
+| Feature | Guide |
+|---|---|
+| Stats, metrics, progress, worker heartbeats | [Monitoring](/java/guides/observability/monitoring) |
+| Micrometer Observation / Sentry middleware | [Integrations](/java/guides/integrations) |
+| React dashboard + REST API | [Dashboard](/java/guides/operations/dashboard) |
+| In-process autoscaling + KEDA scaler endpoint | [Autoscaling](/java/guides/operations/autoscaling) |
+| Standalone CLI | [CLI](/java/guides/operations/cli) |
+
+## Framework integrations
+
+[Spring Boot 3 starter](/java/guides/integrations/spring) — auto-configures a
+`Taskito` bean from `taskito.url` / `taskito.pool-size` / `taskito.namespace`.
+
+
+ Observability and MessagePack dependencies are `compileOnly` — add
+ `io.micrometer:micrometer-observation`, `io.sentry:sentry`, or
+ `org.msgpack:jackson-dataformat-msgpack` only if you use them.
+
diff --git a/docs/content/docs/java/getting-started/concepts.mdx b/docs/content/docs/java/getting-started/concepts.mdx
new file mode 100644
index 00000000..cb945029
--- /dev/null
+++ b/docs/content/docs/java/getting-started/concepts.mdx
@@ -0,0 +1,66 @@
+---
+title: Concepts
+description: "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.
+
+
+
+## Client
+
+A `Taskito` is the handle to one store (SQLite file, Postgres schema, or Redis
+prefix), opened with `Taskito.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. `taskito.queue(name)` returns a handle to a single named queue
+(pause / resume).
+
+```java
+try (Taskito taskito = Taskito.builder().sqlite("taskito.db").open()) { ... }
+```
+
+## Task
+
+A **task** is a named unit of work described by a `Task` — 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.
+
+## Job
+
+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.
+
+## Worker
+
+`taskito.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.
+
+## Result
+
+`taskito.getResult(id, type)` returns the handler's return value once the job
+finishes, deserialized with the queue's
+[serializer](/java/guides/extensibility/serializers). 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](/architecture/overview) for the job lifecycle, scheduler,
+ storage schema, and mesh internals.
+
diff --git a/docs/content/docs/java/getting-started/installation.mdx b/docs/content/docs/java/getting-started/installation.mdx
new file mode 100644
index 00000000..bd05b582
--- /dev/null
+++ b/docs/content/docs/java/getting-started/installation.mdx
@@ -0,0 +1,91 @@
+---
+title: Installation
+description: "Install the Java SDK — Maven Central coordinates, bundled native binaries, three backends."
+---
+
+The Java SDK is a typed shell over the Taskito Rust core, bound through a
+hand-written JNI layer. Baseline: **Java 17**.
+
+
+
+
+```kotlin
+implementation("org.byteveda:taskito:0.18.0")
+annotationProcessor("org.byteveda:taskito-processor:0.18.0") // compile-time TaskHandler bindings
+```
+
+
+
+
+```xml
+
+ org.byteveda
+ taskito
+ 0.18.0
+
+```
+
+```xml
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.byteveda
+ taskito-processor
+ 0.18.0
+
+
+
+
+```
+
+
+
+
+The `taskito-processor` artifact is optional but recommended: it turns
+`@TaskHandler`-annotated methods into typed `Task` constants at compile time —
+zero runtime reflection, GraalVM-native-image friendly. See
+[Tasks](/java/guides/core/tasks#typed-tasks-from-taskhandler).
+
+Companion artifacts: `org.byteveda:taskito-test` (in-memory backend for unit
+tests, no native library) and `org.byteveda:taskito-spring` (Spring Boot 3
+starter).
+
+## Bundled native library
+
+The Rust core ships inside the JAR as a prebuilt library per platform — no
+separate install:
+
+- linux x86_64 / aarch64
+- macOS x86_64 / aarch64
+- Windows x86_64
+
+At runtime the platform binary is extracted (content-addressed, safe under
+concurrent processes) and loaded. Two system properties tune this:
+`-Dtaskito.native.lib=/abs/path` loads an explicit library (local development
+against a fresh build), and `-Dtaskito.native.workdir=/var/lib/myapp/taskito-native`
+relocates extraction on hardened `noexec /tmp` hosts.
+
+
+ On JDK 22+ hot byte operations automatically take a Project Panama (FFM) fast
+ path; older JDKs transparently use JNI. FFM calls a restricted native method,
+ so launch with `--enable-native-access=ALL-UNNAMED` to grant access and
+ silence the JDK warning.
+
+
+## Pick a backend
+
+```java
+import org.byteveda.taskito.Taskito;
+
+Taskito.builder().sqlite("taskito.db").open(); // SQLite (default)
+Taskito.builder().postgres(System.getenv("PG_URL")).open();
+Taskito.builder().redis("redis://localhost").open();
+```
+
+`Taskito.builder().open()` with no backend configured uses SQLite at
+`.taskito/taskito.db`. SQLite needs nothing else — the whole queue lives in one
+file. Next: the [quickstart](/java/getting-started/quickstart).
diff --git a/docs/content/docs/java/getting-started/meta.json b/docs/content/docs/java/getting-started/meta.json
new file mode 100644
index 00000000..c18d0a61
--- /dev/null
+++ b/docs/content/docs/java/getting-started/meta.json
@@ -0,0 +1 @@
+{ "title": "Getting Started", "root": true, "pages": ["installation", "quickstart", "concepts", "capabilities"] }
diff --git a/docs/content/docs/java/getting-started/quickstart.mdx b/docs/content/docs/java/getting-started/quickstart.mdx
new file mode 100644
index 00000000..225e9e40
--- /dev/null
+++ b/docs/content/docs/java/getting-started/quickstart.mdx
@@ -0,0 +1,56 @@
+---
+title: Quickstart
+description: "Define a task, enqueue a job, run a worker, read the result — in Java."
+---
+
+Four steps: open a client, describe a task, enqueue a job, run a worker.
+
+```java
+import com.fasterxml.jackson.core.type.TypeReference;
+import java.time.Duration;
+import java.util.Map;
+import org.byteveda.taskito.Taskito;
+import org.byteveda.taskito.task.Task;
+import org.byteveda.taskito.worker.Worker;
+
+// 1. Describe the task: a name plus its payload type.
+Task