Configure state persistence
By default, Quarkus Flow executes workflows entirely in memory. While this is incredibly fast, it means that if your application crashes, restarts, or is scaled down, any currently running workflow instances are lost.
State Persistence solves this by writing the workflow’s state to a durable datastore every time a task completes or pauses.
Enabling persistence allows your workflows to survive:
-
Asynchronous Callbacks: Pausing a workflow for days or weeks while waiting for a human approval (HITL) or an external webhook.
-
Failures & Restarts: Recovering automatically from unexpected JVM crashes.
-
Kubernetes Node Drains: Safely migrating running workflows across pods during rolling updates, deployments, or cluster scaling events.
(For a deeper conceptual dive into how this enables cluster-wide orchestration, see Durable Workflows in Kubernetes).
|
The Quarkus Flow team is currently actively testing and benchmarking these persistence providers for high-throughput production scenarios. Expect deeper performance tuning guides in the future. |
1. Automatic Restoration
When a persistence provider is active, Quarkus Flow will automatically attempt to resume interrupted workflows upon application startup.
When the server boots, any workflow instance that was running when the JVM previously shut down (as identified by its application ID) will resume execution from its last recorded checkpoint.
If you need to disable this auto-resume behavior, add this to your application.properties:
quarkus.flow.persistence.autoRestore=false
2. Excluding Workflows from Persistence
In some scenarios, you may want to exclude specific workflows from persistence while keeping persistence enabled for others.
To exclude workflows from persistence, add their IDs in namespace:name:version format to the exclusion list in your application.properties:
quarkus.flow.persistence.exclude-workflows=com.example:temporary-workflow:1.0.0,com.example:test-workflow:0.1.0
|
Excluded workflows will:
|
3. Choose a Persistence Provider
Quarkus Flow provides three drop-in persistence implementations. You should only include one of these dependencies in your project at a time.
Option A: Redis (Recommended for distributed workloads)
Redis is an in-memory data structure store that provides excellent performance for high-throughput workflow checkpoints.
<dependency>
<groupId>io.quarkiverse.flow</groupId>
<artifactId>quarkus-flow-redis</artifactId>
</dependency>
Configure the Redis connection in your application.properties:
quarkus.redis.hosts=redis://localhost:6379
| Our integration tests use Valkey image instead. |
Using Infinispan Server RESP
Quarkus Flow can use Infinispan Server as a Redis-compatible persistence backend through the Infinispan RESP endpoint.
This mode still uses the quarkus-flow-redis persistence implementation and the Quarkus Redis client. No Quarkus Flow Infinispan-specific runtime dependency is required.
Infinispan Server exposes RESP on the default single-port endpoint. Redis clients connect to the same server port, 11222, and Infinispan routes RESP traffic to the RESP connector.
<dependency>
<groupId>io.quarkiverse.flow</groupId>
<artifactId>quarkus-flow-redis</artifactId>
</dependency>
Configure the Quarkus Redis client to point to the Infinispan Server RESP endpoint:
quarkus.redis.hosts=redis://admin:password@infinispan-server:11222
For local development with Docker:
docker run --rm \
--name infinispan \
-p 11222:11222 \
-e USER=admin \
-e PASS=password \
quay.io/infinispan/server:15.0
Then configure your application:
quarkus.redis.hosts=redis://admin:password@localhost:11222
For Kubernetes deployments managed by the Infinispan Operator, point quarkus.redis.hosts to the internal Infinispan service name:
quarkus.redis.hosts=redis://admin:password@infinispan:11222
A minimal Infinispan cluster managed by the Operator can be created with:
apiVersion: infinispan.org/v1
kind: Infinispan
metadata:
name: infinispan
spec:
replicas: 2
Use Kubernetes Secret or your platform secret-management mechanism to provide the username and password to the Quarkus application instead of hardcoding credentials in application.properties.
For example:
quarkus.redis.hosts=redis://${INFINISPAN_USER}:${INFINISPAN_PASSWORD}@infinispan:11222
Option B: JPA (Relational Databases)
JPA allows you to store workflow state in a standard relational database (e.g., PostgreSQL, MySQL). This is ideal if your enterprise architecture already relies heavily on a relational database.
<dependency>
<groupId>io.quarkiverse.flow</groupId>
<artifactId>quarkus-flow-jpa</artifactId>
</dependency>
You must also configure a Quarkus JDBC driver and datasource. See the Quarkus Datasources Guide for full instructions.
Database Schema Migration
Quarkus Flow can create the persistence tables automatically through the Quarkus Hibernate ORM schema management configuration:
quarkus.hibernate-orm.database.generation=update
This is convenient for development and testing. For production, prefer managing the schema explicitly with a migration tool such as Flyway.
Add the Flyway extension (and the matching database support artifact, if required for your database):
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-flyway</artifactId>
</dependency>
Enable migration at startup and disable Hibernate ORM schema generation in your application.properties:
quarkus.hibernate-orm.database.generation=none
quarkus.flyway.enabled=true
quarkus.flyway.migrate-at-start=true
Then create a migration script at src/main/resources/db/migration/V1__create_tables.sql with the three tables required by the JPA provider:
- + H2
CREATE TABLE cloud_event_entity
(
id VARCHAR(255) NOT NULL,
reg_id VARCHAR(255) NOT NULL,
source VARCHAR(255) NOT NULL,
type VARCHAR(255) NOT NULL,
subject VARCHAR(255),
data_content_type VARCHAR(255),
data_schema VARCHAR(255),
time TIMESTAMP(6) WITH TIME ZONE,
data VARBINARY,
extensions VARBINARY,
processed_flag BOOLEAN DEFAULT FALSE,
version TINYINT NOT NULL CHECK (version BETWEEN 0 AND 1),
PRIMARY KEY (id)
);
CREATE TABLE process_instance_entity
(
application_id VARCHAR(255) NOT NULL,
instance_id VARCHAR(255) NOT NULL,
workflow_name VARCHAR(255) NOT NULL,
workflow_namespace VARCHAR(255) NOT NULL,
workflow_version VARCHAR(255) NOT NULL,
started_at TIMESTAMP(6) WITH TIME ZONE NOT NULL,
status TINYINT CHECK (status BETWEEN 0 AND 6),
input VARBINARY,
PRIMARY KEY (application_id, instance_id)
);
CREATE TABLE task_info_entity
(
application_id VARCHAR(255) NOT NULL,
process_instance_id VARCHAR(255) NOT NULL,
json_pointer VARCHAR(255) NOT NULL,
iteration INTEGER NOT NULL,
task_type INTEGER NOT NULL CHECK (task_type IN (1, 2)),
is_end_node BOOLEAN,
retry_attempt SMALLINT,
instant TIMESTAMP(6) WITH TIME ZONE,
next_position VARCHAR(255),
context VARBINARY,
model VARBINARY,
PRIMARY KEY (iteration, application_id, json_pointer, process_instance_id),
CHECK (task_type <> 1 OR (is_end_node IS NOT NULL)),
CHECK (task_type <> 2 OR (retry_attempt IS NOT NULL)),
CONSTRAINT fk_task_process_instance
FOREIGN KEY (application_id, process_instance_id)
REFERENCES process_instance_entity (application_id, instance_id)
);
- + MySQL
CREATE TABLE cloud_event_entity
(
id VARCHAR(255) NOT NULL,
reg_id VARCHAR(255) NOT NULL,
source VARCHAR(255) NOT NULL,
type VARCHAR(255) NOT NULL,
subject VARCHAR(255),
data_content_type VARCHAR(255),
data_schema VARCHAR(255),
`time` DATETIME(6),
data LONGBLOB,
extensions LONGBLOB,
processed_flag BOOLEAN DEFAULT FALSE,
version TINYINT NOT NULL CHECK (version BETWEEN 0 AND 1),
PRIMARY KEY (id)
);
CREATE TABLE process_instance_entity
(
application_id VARCHAR(255) NOT NULL,
instance_id VARCHAR(255) NOT NULL,
workflow_name VARCHAR(255) NOT NULL,
workflow_namespace VARCHAR(255) NOT NULL,
workflow_version VARCHAR(255) NOT NULL,
started_at DATETIME(6) NOT NULL,
status TINYINT CHECK (status BETWEEN 0 AND 6),
input LONGBLOB,
PRIMARY KEY (application_id, instance_id)
);
CREATE TABLE task_info_entity
(
application_id VARCHAR(255) NOT NULL,
process_instance_id VARCHAR(255) NOT NULL,
json_pointer VARCHAR(255) NOT NULL,
iteration INTEGER NOT NULL,
task_type INTEGER NOT NULL CHECK (task_type IN (1, 2)),
is_end_node BOOLEAN,
retry_attempt SMALLINT,
instant DATETIME(6),
next_position VARCHAR(255),
context LONGBLOB,
model LONGBLOB,
PRIMARY KEY (iteration, application_id, json_pointer, process_instance_id),
CHECK (task_type <> 1 OR (is_end_node IS NOT NULL)),
CHECK (task_type <> 2 OR (retry_attempt IS NOT NULL)),
CONSTRAINT fk_task_process_instance
FOREIGN KEY (application_id, process_instance_id)
REFERENCES process_instance_entity (application_id, instance_id)
);
- + PostgreSQL
CREATE TABLE cloud_event_entity
(
id VARCHAR(255) NOT NULL,
reg_id VARCHAR(255) NOT NULL,
source VARCHAR(255) NOT NULL,
type VARCHAR(255) NOT NULL,
subject VARCHAR(255),
data_content_type VARCHAR(255),
data_schema VARCHAR(255),
time TIMESTAMP(6) WITH TIME ZONE,
data BYTEA,
extensions BYTEA,
processed_flag BOOLEAN DEFAULT FALSE,
version SMALLINT NOT NULL CHECK (version BETWEEN 0 AND 1),
PRIMARY KEY (id)
);
CREATE TABLE process_instance_entity
(
application_id VARCHAR(255) NOT NULL,
instance_id VARCHAR(255) NOT NULL,
workflow_name VARCHAR(255) NOT NULL,
workflow_namespace VARCHAR(255) NOT NULL,
workflow_version VARCHAR(255) NOT NULL,
started_at TIMESTAMP(6) WITH TIME ZONE NOT NULL,
status SMALLINT CHECK (status BETWEEN 0 AND 6),
input BYTEA,
PRIMARY KEY (application_id, instance_id)
);
CREATE TABLE task_info_entity
(
application_id VARCHAR(255) NOT NULL,
process_instance_id VARCHAR(255) NOT NULL,
json_pointer VARCHAR(255) NOT NULL,
iteration INTEGER NOT NULL,
task_type INTEGER NOT NULL CHECK (task_type IN (1, 2)),
is_end_node BOOLEAN,
retry_attempt SMALLINT,
instant TIMESTAMP(6) WITH TIME ZONE,
next_position VARCHAR(255),
context BYTEA,
model BYTEA,
PRIMARY KEY (iteration, application_id, json_pointer, process_instance_id),
CHECK (task_type <> 1 OR (is_end_node IS NOT NULL)),
CHECK (task_type <> 2 OR (retry_attempt IS NOT NULL)),
CONSTRAINT fk_task_process_instance
FOREIGN KEY (application_id, process_instance_id)
REFERENCES process_instance_entity (application_id, instance_id)
);
- + Oracle
CREATE TABLE cloud_event_entity
(
id VARCHAR2(255) NOT NULL,
reg_id VARCHAR2(255) NOT NULL,
source VARCHAR2(255) NOT NULL,
type VARCHAR2(255) NOT NULL,
subject VARCHAR2(255),
data_content_type VARCHAR2(255),
data_schema VARCHAR2(255),
time TIMESTAMP(6) WITH TIME ZONE,
data BLOB,
extensions BLOB,
processed_flag NUMBER(1, 0) DEFAULT 0,
version NUMBER(3, 0) NOT NULL CHECK (version BETWEEN 0 AND 1),
PRIMARY KEY (id)
);
CREATE TABLE process_instance_entity
(
application_id VARCHAR2(255) NOT NULL,
instance_id VARCHAR2(255) NOT NULL,
workflow_name VARCHAR2(255) NOT NULL,
workflow_namespace VARCHAR2(255) NOT NULL,
workflow_version VARCHAR2(255) NOT NULL,
started_at TIMESTAMP(6) WITH TIME ZONE NOT NULL,
status NUMBER(3, 0) CHECK (status BETWEEN 0 AND 6),
input BLOB,
PRIMARY KEY (application_id, instance_id)
);
CREATE TABLE task_info_entity
(
application_id VARCHAR2(255) NOT NULL,
process_instance_id VARCHAR2(255) NOT NULL,
json_pointer VARCHAR2(255) NOT NULL,
iteration NUMBER(10, 0) NOT NULL,
task_type NUMBER(10, 0) NOT NULL CHECK (task_type IN (1, 2)),
is_end_node NUMBER(1, 0),
retry_attempt NUMBER(5, 0),
instant TIMESTAMP(6) WITH TIME ZONE,
next_position VARCHAR2(255),
context BLOB,
model BLOB,
PRIMARY KEY (iteration, application_id, json_pointer, process_instance_id),
CHECK (task_type <> 1 OR (is_end_node IS NOT NULL)),
CHECK (task_type <> 2 OR (retry_attempt IS NOT NULL)),
CONSTRAINT fk_task_process_instance
FOREIGN KEY (application_id, process_instance_id)
REFERENCES process_instance_entity (application_id, instance_id)
);
- + MSSQL
CREATE TABLE cloud_event_entity
(
id VARCHAR(255) NOT NULL,
reg_id VARCHAR(255) NOT NULL,
source VARCHAR(255) NOT NULL,
type VARCHAR(255) NOT NULL,
subject VARCHAR(255),
data_content_type VARCHAR(255),
data_schema VARCHAR(255),
[time] DATETIMEOFFSET(6),
data VARBINARY(MAX),
extensions VARBINARY(MAX),
processed_flag BIT DEFAULT 0,
version TINYINT NOT NULL CHECK (version BETWEEN 0 AND 1),
PRIMARY KEY (id)
);
CREATE TABLE process_instance_entity
(
application_id VARCHAR(255) NOT NULL,
instance_id VARCHAR(255) NOT NULL,
workflow_name VARCHAR(255) NOT NULL,
workflow_namespace VARCHAR(255) NOT NULL,
workflow_version VARCHAR(255) NOT NULL,
started_at DATETIMEOFFSET(6) NOT NULL,
status TINYINT CHECK (status BETWEEN 0 AND 6),
input VARBINARY(MAX),
PRIMARY KEY (application_id, instance_id)
);
CREATE TABLE task_info_entity
(
application_id VARCHAR(255) NOT NULL,
process_instance_id VARCHAR(255) NOT NULL,
json_pointer VARCHAR(255) NOT NULL,
iteration INT NOT NULL,
task_type INT NOT NULL CHECK (task_type IN (1, 2)),
is_end_node BIT,
retry_attempt SMALLINT,
instant DATETIMEOFFSET(6),
next_position VARCHAR(255),
context VARBINARY(MAX),
model VARBINARY(MAX),
PRIMARY KEY (iteration, application_id, json_pointer, process_instance_id),
CHECK (task_type <> 1 OR (is_end_node IS NOT NULL)),
CHECK (task_type <> 2 OR (retry_attempt IS NOT NULL)),
CONSTRAINT fk_task_process_instance
FOREIGN KEY (application_id, process_instance_id)
REFERENCES process_instance_entity (application_id, instance_id)
);
|
Each tab provides the migration script for a specific database. Pick the tab that matches your target database and adapt it further if your schema or naming conventions differ.
Note that the |
Option C: MVStore (Local file system)
MVStore writes workflow state to a local file system file. This is an excellent, zero-infrastructure option for local development, testing, or single-node edge deployments.
<dependency>
<groupId>io.quarkiverse.flow</groupId>
<artifactId>quarkus-flow-mvstore</artifactId>
</dependency>
Configure the local file path in your application.properties:
quarkus.flow.persistence.mvstore.db-path=${user.home}/testFlowMVStore.db
See also
-
Durable Workflows in Kubernetes — architecture and concepts for distributed execution.
-
Using the Quarkus Redis Client — advanced Redis configuration.
-
MVStore homepage — deeper dive into the MVStore engine.
-
Serverless Workflow Java SDK Persistence — upstream SDK reference.