Skip to content

About

Official Java SDK and Spring Boot starter for QuantumBPM: BPMN 2.0 workflow orchestration, DMN 1.5 decision evaluation, and virtual-thread job workers.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

QuantumBPM Java SDK

Official Java SDK for the QuantumBPM platform - DMN evaluation, BPMN process orchestration, and external job workers.

Installation

The SDK is split into two artifacts:

<!-- Plain Java client -->
<dependency>
    <groupId>com.quantumbpm</groupId>
    <artifactId>quantum-client</artifactId>
    <version>1.2.0</version>
</dependency>

<!-- Spring Boot starter - autoconfigured client + @JobWorker registration -->
<dependency>
    <groupId>com.quantumbpm</groupId>
    <artifactId>quantum-spring</artifactId>
    <version>1.2.0</version>
</dependency>

Java 21+. The worker runtime uses virtual threads.

What's in the box

Module Package Purpose
quantum-client com.quantumbpm.client Plain Java SDK - QuantumBPM, DmnClient, BpmnClient, Worker
com.quantumbpm.client.auth TokenProvider, ZitadelTokenProvider, StaticTokenProvider
com.quantumbpm.client.variables Vars wrapper with typed accessors and FEEL-context conversion
com.quantumbpm.client.workers Worker, Job<T>, Handler<T>, BpmnError
com.quantumbpm.client.generated OpenAPI-generated client (use client.raw() for unwrapped endpoints)
quantum-spring com.quantumbpm.spring Spring Boot starter, @JobWorker annotation, autoconfig

Quick start (plain Java)

import com.quantumbpm.client.QuantumBPM;
import com.quantumbpm.client.auth.ZitadelTokenProvider;
import com.quantumbpm.client.variables.Vars;
import java.util.UUID;

ZitadelTokenProvider provider = new ZitadelTokenProvider(
    "./service-account.json",       // Zitadel JSON Key file
    "https://auth.quantumbpm.com",  // issuer
    "your-zitadel-project-id");     // audience scope

QuantumBPM client = QuantumBPM.builder()
    .baseUrl("https://api.quantumbpm.com")
    .projectId(UUID.fromString("00000000-0000-0000-0000-000000000000"))
    .tokenProvider(provider)
    .build();

var result = client.dmn().evaluate(
    "loan-eligibility",
    new Vars().set("requestedAmt", 1000).set("creditScore", 720));
System.out.println(result);

Quick start (Spring Boot)

application.yml:

quantumbpm:
  base-url: https://api.quantumbpm.com
  project-id: 00000000-0000-0000-0000-000000000000
  auth:
    zitadel:
      key-file: /path/to/service-account.json
      issuer: https://auth.quantumbpm.com
      project-id: 123456789
  worker:
    enabled: true            # default; scans @JobWorker beans
    client-id: billing-svc

A handler - register a method with @JobWorker:

import com.quantumbpm.client.variables.Vars;
import com.quantumbpm.client.workers.Job;
import com.quantumbpm.spring.JobWorker;
import org.springframework.stereotype.Component;

@Component
public class EmailHandler {

    @JobWorker(type = "send-email", maxJobs = 5, lockDuration = "1m")
    public Vars handle(Job<EmailJob> job) {
        emailService.send(job.typed().recipient(), job.typed().subject());
        return new Vars().set("messageID", "msg-123");
    }

    public record EmailJob(String recipient, String subject) {}
}

The autoconfig wires everything: builds the QuantumBPM bean from properties, scans every Spring bean for @JobWorker methods, and starts/stops the worker around the application lifecycle.

Inject QuantumBPM anywhere you need DMN/BPMN client calls:

@Service
public class LoanService {
    private final QuantumBPM quantum;
    public LoanService(QuantumBPM quantum) { this.quantum = quantum; }

    public String startLoan(LoanRequest req) throws Exception {
        return quantum.bpmn().startInstance(processDefId, Vars.of()
            .set("applicantID", req.applicantId())
            .set("requestedAmt", req.amount()));
    }
}

Authentication

The TokenProvider interface is a @FunctionalInterface returning a bearer token. Two implementations ship out of the box.

Against the local devserver, which has no login, leave the token provider out. The API lives under /api there:

QuantumBPM client = QuantumBPM.builder()
    .baseUrl("http://localhost:9060/api")
    .projectId("00000000-0000-0000-0000-0000000000cc")
    .build();

The Spring Boot starter does the same when neither quantumbpm.token nor quantumbpm.auth.zitadel.key-file is set:

quantumbpm:
  base-url: http://localhost:9060/api
  project-id: 00000000-0000-0000-0000-0000000000cc

Zitadel service account

import com.quantumbpm.client.auth.ZitadelTokenProvider;

var provider = new ZitadelTokenProvider(
    "./service-account.json",      // path to JSON Key file
    "https://auth.quantumbpm.com", // issuer URL
    "your-zitadel-project-id");    // adds the audience scope

Tokens are cached in-memory until shortly before expiry. Concurrent calls share the cache via a lock.

Static bearer token

import com.quantumbpm.client.auth.StaticTokenProvider;

var provider = new StaticTokenProvider("eyJhbGciOi...");

Bring your own

TokenProvider provider = () -> myAuth.fetchToken();

DMN evaluation

client.dmn() exposes four methods.

Evaluate a stored definition

var result = client.dmn().evaluate(
    "loan-eligibility",
    new Vars().set("requestedAmt", 5000).set("creditScore", 720));

Returns Map<String, EvaluationResult> keyed by decision name.

Decode one decision's value into a record or POJO that matches its output type. Numbers stay exact as BigDecimal. It throws when the decision is missing from the result or failed to evaluate:

import static com.quantumbpm.client.dmn.DmnClient.decisionValue;

record CoverCheck(BigDecimal limit, boolean withinCover, BigDecimal uncovered, String message) {}

CoverCheck cover = decisionValue(result, "Cover check", CoverCheck.class);

Pin a version, restrict the evaluated decisions, or attach decision services:

import static com.quantumbpm.client.dmn.DmnClient.*;

var result = client.dmn().evaluate(
    "loan-eligibility", vars,
    withVersion(3),
    withDecisions("eligibility", "rate"));

Evaluate by platform UUID

var result = client.dmn().evaluateById(definitionUuid, vars);

Ad-hoc XML evaluation

import static com.quantumbpm.client.dmn.DmnClient.*;

var result = client.dmn().evaluateDesign(
    dmnXml, vars,
    withAdditionalXmls(importedXml1, importedXml2),
    withDesignDecisions("eligibility"));

Batch ad-hoc evaluation

var rows = List.of(
    new Vars().set("requestedAmt", 1000),
    new Vars().set("requestedAmt", 5000),
    new Vars().set("requestedAmt", 25000));
var batch = client.dmn().evaluateDesignBatch(dmnXml, rows);

BPMN processes

client.bpmn() covers the full BPMN runtime surface.

Deploy and start

var draft = client.bpmn().createResource("loan-process", bpmnXml);
client.bpmn().deployResource(draft.getId());

// Re-fetch to get populated process-definition list.
var deployed = client.bpmn().getResource(draft.getId());
var processDef = deployed.getProcesses().get(0);

String workflowId = client.bpmn().startInstance(
    processDef.getId(),
    new Vars().set("applicantID", "u-123").set("requestedAmt", 25000));

Inspect runtime state

var state = client.bpmn().getInstance(workflowId);
System.out.println(state.getStatus());

Vars vars = client.bpmn().getInstanceVariables(workflowId);
var children = client.bpmn().getInstanceChildren(workflowId);

Send messages and signals

client.bpmn().publishMessage("loan-approved",
    new Vars().set("approvedAmt", 24000),
    null,                  // CorrelationKeys (optional)
    "PT5M");               // TTL (optional)

client.bpmn().publishSignal("system-maintenance", null);

User tasks

var page = client.bpmn().listUserTasks(
    null, "CREATED", "alice@example.com", null, null, null, null);

client.bpmn().completeUserTask(executionKey, new Vars().set("approved", true));

// Or fail with a BPMN error code:
client.bpmn().throwUserTaskError(executionKey, "REVIEW_REJECTED", null);

External job workers

Workers handle service tasks asynchronously. Register a handler per task type, then start the worker. The runtime owns long-polling, lock heartbeats, dispatch (on virtual threads), and outcome mapping.

Plain Java (no Spring)

import com.quantumbpm.client.QuantumBPM;
import com.quantumbpm.client.variables.Vars;
import com.quantumbpm.client.workers.BpmnError;
import com.quantumbpm.client.workers.Worker;

QuantumBPM client = QuantumBPM.builder()...build();
Worker worker = client.newWorker("billing-svc");

worker.handle("send-email", job -> {
    String recipient = job.vars().get("recipient", String.class);
    String subject = job.vars().get("subject", String.class);
    emailer.send(recipient, subject);
    return new Vars().set("messageID", "msg-123");          // → Complete
}, Worker.withMaxJobs(10), Worker.withLockDuration("1m"));

worker.start();    // returns immediately. Loops run on virtual threads
Runtime.getRuntime().addShutdownHook(new Thread(() -> worker.stop(20_000)));
worker.awaitTermination();    // blocks until stop returns

Virtual threads do not keep the JVM alive, so awaitTermination() blocks main until stop has returned. See Locks, heartbeats, and shutdown for what stop does.

Typed handlers

Pass the typed class as the second argument; the runtime decodes the job's variables into that type before invoking the handler. The decoded value lands in job.typed().

record EmailJob(String recipient, String subject) {}

worker.handle("send-email", EmailJob.class, job -> {
    emailer.send(job.typed().recipient(), job.typed().subject());
    return new Vars().set("messageID", "msg-123");
});

In Spring, the type is inferred from the method's Job<T> parameter - no extra argument needed.

Throwing typed BPMN errors

Throw a BpmnError to fail the job with a code that boundary error events on the originating service task can catch:

worker.handle("charge-card", job -> {
    try {
        return new Vars().set("transactionID", charge(job.vars().toMap()));
    } catch (InsufficientFundsException e) {
        throw new BpmnError("INSUFFICIENT_FUNDS",
            new Vars().set("availableBalance", 12.0));
    }
});

Any other exception is reported as WORKER_ERROR, which the server treats as a retryable failure that decrements the job's retry budget.

Concurrency, polling, and locks

worker.handle("send-email", handler,
    Worker.withMaxJobs(10),         // up to 10 in flight per task type
    Worker.withPollTimeout("45s"),  // long-poll wait
    Worker.withLockDuration("2m")); // exclusive lock per job

Concurrency is per task type. Different task types poll independently. Each poll asks only for as many jobs as there are free slots, so no job waits on the client without a heartbeat.

Durations are Go duration strings such as 500ms, 30s, 12.5s, 1m30s or 1.5h. An invalid one throws IllegalArgumentException from handle.

Locks, heartbeats, and shutdown

A polled job is locked to this worker for lockDuration past its latest heartbeat. The server clamps the lock to between 10 seconds and 5 minutes by default (BPMN_WORKER_LEASE_MIN, BPMN_WORKER_LEASE_MAX) and returns the expiry it granted with each job. While the handler runs, the worker heartbeats at half of the lock the server granted. The server caps one attempt at 30 minutes by default (BPMN_JOB_MAX_HOLD). When a lock runs out, the server charges the job a retry and offers it again, or raises an incident when no retries are left. So an abandoned job always costs a retry.

When a heartbeat finds the lock gone (another worker holds the job, or it finished or was canceled), the runtime stops heartbeating, interrupts the handler and does not report its result. A complete or error sent after the lock is gone is discarded by the server and logged as a warning.

worker.stop(timeoutMillis) stops polling at once. Running handlers keep running and heartbeating, and their results are reported. Handlers still running at the timeout are interrupted and not reported, and stop returns. Their locks then run out on the server. Handlers that block (I/O, sleeps, waits) should respond to interrupts.

Wire stop to SIGTERM with a shutdown hook:

Runtime.getRuntime().addShutdownHook(new Thread(() -> worker.stop(20_000)));

The Spring starter calls stop(20_000) when the application context closes. On Kubernetes, set terminationGracePeriodSeconds above the stop timeout (for example 30 for a 20 s timeout). Otherwise the pod is killed while handlers are still finishing.

Variables

Vars is a thin wrapper around Map<String, Object> shared by DMN, BPMN, and workers.

Construction

var v = new Vars().set("amount", 100).set("name", "Alice");
var v = Vars.from(Map.of("amount", 100, "name", "Alice"));

Typed access

double amount = v.get("amount", Double.class);
boolean approved = v.get("approved", Boolean.class);

record Loan(double requestedAmt, boolean approved) {}
Loan loan = v.as(Loan.class);

get(name, Class) and as(Class) use Jackson under the hood - records, POJOs, and primitives all work.

Escape hatch

client.raw() exposes the underlying generated ApiClient for endpoints not yet wrapped (instance migration, modification, ad-hoc triggers, batch job complete/error, etc.):

import com.quantumbpm.client.generated.api.BpmnApi;

var api = new BpmnApi(client.raw());
api.migrateBpmnInstance(client.projectId(), workflowId, body);

License

MIT License - see LICENSE for details.

About

Official Java SDK and Spring Boot starter for QuantumBPM: BPMN 2.0 workflow orchestration, DMN 1.5 decision evaluation, and virtual-thread job workers.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages