Developer Guide
This section covers the data model, configuration reference, and extension points available to developers integrating Process Manager into an application.
Data Model
Process Manager persists two entities: a process definition and its execution history. Each process holds a cron schedule, a reference to the Runnable bean that will be executed, and two independent status columns. Each execution record captures the start time, end time, and duration of a single run.
---
config:
layout: elk
---
erDiagram
pm_process {
integer id PK
varchar name
varchar schedule_status
varchar execution_status
varchar schedule
varchar job_qualified_name
}
pm_execution {
integer id PK
date start_date
time start_time
date end_date
time end_time
bigint duration
integer process_id FK
}
pm_process ||--o{ pm_execution : "has"
Database Schema
Process Manager manages two tables. The schema below represents the DDL generated by Hibernate for a standard relational database. Column names follow Hibernate's default snake_case naming strategy.
CREATE SEQUENCE pm_process_seq
START WITH 1
INCREMENT BY 50;
CREATE SEQUENCE pm_execution_seq
START WITH 1
INCREMENT BY 50;
CREATE TABLE pm_process (
id INTEGER NOT NULL,
name VARCHAR(255),
schedule_status VARCHAR(255),
execution_status VARCHAR(255),
schedule VARCHAR(255),
job_qualified_name VARCHAR(255),
CONSTRAINT pk_pm_process PRIMARY KEY (id)
);
CREATE TABLE pm_execution (
id INTEGER NOT NULL,
start_date DATE,
start_time TIME,
end_date DATE,
end_time TIME,
duration BIGINT,
process_id INTEGER NOT NULL,
CONSTRAINT pk_pm_execution PRIMARY KEY (id),
CONSTRAINT fk_pm_execution_process FOREIGN KEY (process_id) REFERENCES pm_process (id)
);
The schedule_status and execution_status columns store the string representation of the ScheduleStatus and ExecutionStatus enums respectively. The schedule column holds a standard six-field cron expression. The job_qualified_name column stores the fully qualified class name of the Runnable bean assigned to the process.
The start_date, start_time, end_date, and end_time columns of pm_execution are stored in UTC. Conversion to the time zone of the browser happens in the presentation layer.
The sequences are created by Hibernate when GenerationType.AUTO resolves to a sequence-based strategy, which is the default behaviour in Hibernate 6.
Module Overview
Process Manager is structured as six Maven modules following the AppJars layered architecture:
| Module | Artifact ID | Description |
|---|---|---|
| Model | appjars-process-manager-model |
DTOs, enums, filters, exceptions, auto-configuration |
| Business API | appjars-process-manager-business |
ProcessService and ProcessExecutionService interfaces |
| Business Impl | appjars-process-manager-business-impl |
Service implementations with scheduling logic |
| Data API | appjars-process-manager-data |
ProcessDao and ProcessExecutionDao interfaces |
| Data Impl | appjars-process-manager-data-impl |
JPA entities and DAO implementations |
| Flow UI | appjars-process-manager-flow |
Vaadin views, dialogs, and route configuration |
A monolithic application includes the three implementation modules (-business-impl, -data-impl, -flow). The API modules (-business, -data) are pulled in transitively.
Spring Auto-Configuration
Process Manager registers itself through Spring Boot's auto-configuration mechanism. The entry point is ProcessManagerAutoConfiguration, declared in:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
This class scans the com.appjars.processmanager package for all Spring components and JPA entities, so no additional @EntityScan or @ComponentScan declarations are required in the host application beyond those described in the Getting Started guide.
On application startup, the service listens to ContextRefreshedEvent to restore scheduled processes. It loads all persisted processes and normalises them:
- Any process whose
Runnablebean can no longer be resolved is transitioned toTASK_NOT_FOUND. - Any process left with a null schedule status is defaulted to
UNSCHEDULED. - Any process still marked
RUNNINGfrom a previous run — a state that cannot survive a restart — is reset toREADY. - A process that fails to normalise is logged and skipped, so a single bad row cannot abort startup.
Processes whose schedule status is ENABLED then have their cron triggers re-registered. On shutdown, a @PreDestroy hook resets stale RUNNING processes and cancels every registered trigger.
Scheduling
Process Manager owns the scheduler it runs jobs on. ProcessManagerSchedulerConfiguration declares a ThreadPoolTaskScheduler under the dedicated bean name processManagerTaskScheduler, built from Spring Boot's ThreadPoolTaskSchedulerBuilder, and ProcessServiceImpl injects it by name.
Two consequences matter for the host application:
@EnableSchedulingis not required. That annotation only registers the post-processor that drives@Scheduledmethods, which this appjar does not use. Owning the scheduler avoids activating scheduling globally as a side effect of adding the appjar.- No bean clash. Because the scheduler is injected by name, an application that declares its own
TaskScheduler— including one that does use@EnableSchedulingfor its own@Scheduledmethods — does not interfere with Process Manager.
The scheduler honours the standard spring.task.scheduling.* properties, and its threads carry the process-manager-scheduler- name prefix.
Configuration Properties
The following properties can be set in application.properties to customise the behaviour of Process Manager:
| Property | Default | Description |
|---|---|---|
com.appjars.processmanager.url.process |
pm/process |
URL path of the process management view |
com.appjars.processmanager.executionsdialog.datetimeformat |
dd/MM/yyyy HH:mm:ss.SS |
Pattern used for the Start and End columns of the execution history grid |
com.appjars.processmanager.executionsdialog.dateformat |
dd/MM/yyyy |
Pattern used for the date part of the execution history filters |
Process Status Lifecycle
A process carries two orthogonal statuses, persisted in separate columns and modelled by separate enums. The schedule status describes the state of the cron trigger; the execution status describes whether the task is running. Executing a process never overwrites its schedule status, so a process that is running still records the schedule it belongs to.
Schedule status
stateDiagram-v2
[*] --> PAUSED : save with schedule
[*] --> UNSCHEDULED : save without schedule
PAUSED --> ENABLED : resumeProcess()
ENABLED --> PAUSED : pauseProcess()
ENABLED --> UNSCHEDULED : disableProcess()
PAUSED --> UNSCHEDULED : disableProcess()
TASK_NOT_FOUND --> UNSCHEDULED : disableProcess()
ENABLED --> TASK_NOT_FOUND : task missing at startup
PAUSED --> TASK_NOT_FOUND : task missing at startup
| Status | Description |
|---|---|
ENABLED |
A cron trigger is registered and will fire |
PAUSED |
The schedule is preserved but its trigger is cancelled |
UNSCHEDULED |
No schedule is defined for the process |
TASK_NOT_FOUND |
The Runnable referenced by job_qualified_name no longer resolves |
resumeProcess() is valid only from ENABLED and PAUSED; any other status raises InvalidProcessStatusTransition. pauseProcess() is valid only from ENABLED. disableProcess() clears the cron expression and sets UNSCHEDULED regardless of the current status.
Execution status
stateDiagram-v2
[*] --> READY
READY --> RUNNING : execution starts
RUNNING --> READY : execution ends
| Status | Description |
|---|---|
RUNNING |
The process task is executing |
READY |
The process is not executing |
runProcess() raises InvalidProcessStatusTransition when the process is already RUNNING. Saving or updating a process resets its execution status to READY.
Real-Time Status Propagation
Both statuses reach the browser without polling. The business layer publishes a Spring application event on every change — ProcessScheduleStatusChangedEvent from ProcessServiceImpl, and ProcessExecutionStatusChangedEvent from ScheduledTaskPersistenceServiceImpl — carrying the process id and the new status. Neither class depends on the UI.
flowchart TD
A[Status changes in the business layer] --> B[Spring application event]
B --> C[ProcessStatusSignals]
C --> D[SharedValueSignal per process]
D --> E[Badges and row actions in every open ProcessListView]
ProcessStatusSignals is a singleton that listens for both events and holds one SharedValueSignal per process and per status kind. ProcessListView binds its badges and its pause, resume, and menu controls to those signals, so a change made by a background scheduler thread or by another administrator repaints every open view. No UI.access() call is involved.
Because propagation happens over server push, the host application must enable it with @Push on its AppShellConfigurator class. Without it, the badges only reflect the state at the moment the view was loaded.
Task Discovery
Process Manager discovers background tasks by scanning the Spring application context for beans that implement java.lang.Runnable. Each discovered bean is wrapped in an AssignableTask and made available for assignment when creating or editing a process.
To register a task, create a @Component that implements Runnable:
@Component
public class ReportGenerationTask implements Runnable {
@Override
public void run() {
// task logic
}
}
The class name displayed in the UI is derived from the bean's simple class name. The fully qualified class name is persisted in job_qualified_name so the scheduler can reconstruct the task after a restart.
Service API
ProcessService
ProcessService is the primary interface for managing process definitions. It extends CrudService and exposes the following methods specific to the module:
// Retrieve a paginated, filtered, and sorted stream of processes
Stream<ProcessDto> getProcesses(int offset, int limit, ProcessFilter filter, List<ProcessSort> sortOrder);
// Count processes matching a filter
Integer countProcesses(int offset, int limit, ProcessFilter filter);
// Return all Runnable beans available for assignment
Set<AssignableTask> getTasks() throws ClassNotFoundException;
// Register the cron trigger and transition the schedule status to ENABLED
void resumeProcess(ProcessDto process) throws ClassNotFoundException;
// Cancel the trigger and transition the schedule status to PAUSED
void pauseProcess(ProcessDto process);
// Clear the schedule and transition the schedule status to UNSCHEDULED
void disableProcess(ProcessDto process);
// Execute the process once immediately, regardless of its schedule
void runProcess(ProcessDto process) throws ClassNotFoundException;
// Persist a process after a run, without applying the scheduling side effects of update()
void updateOnRun(ProcessDto process);
resumeProcess(), pauseProcess(), and runProcess() throw InvalidProcessStatusTransition when called on a process in a state that does not allow the requested transition. getTasks(), resumeProcess(), and runProcess() throw ClassNotFoundException when the Runnable referenced by the process cannot be resolved in the application context.
ProcessFilter carries id, name, scheduleStatus, and executionStatus; the two status fields filter on the corresponding columns independently.
ScheduledTaskPersistenceService
Persisting the state of a running process is delegated to a separate service so that the transaction that records progress is not rolled back by a failure inside the task itself:
// Persist the process as RUNNING before the task is invoked
void updateProcessBeforeExecution(ProcessDto process);
// Persist the process as READY and store the execution record after the task returns
void saveExecutionAndUpdateProcess(ProcessDto process, ProcessExecutionDto execution);
Both methods publish a ProcessExecutionStatusChangedEvent after committing.
ProcessExecutionService
ProcessExecutionService provides read access to the execution history:
// Retrieve a paginated, filtered, and sorted stream of execution records
Stream<ProcessExecutionDto> getProcessExecutions(int offset, int limit, ProcessExecutionFilter filter, List<ProcessExecutionSort> sortOrder);
// Count execution records matching a filter
Integer countProcessExecutions(int offset, int limit, ProcessExecutionFilter filter);
The bounds carried by ProcessExecutionFilter (startDate, startTime, endDate, endTime) are expressed in UTC, matching the stored columns. Each is optional and applied independently, so a filter may constrain a date without constraining a time.
Execution Flow
Every run — scheduled or triggered manually — is wrapped in a ScheduledTask and submitted to the appjar's own scheduler. It performs the following steps:
- The execution status is set to
RUNNINGand persisted. The schedule status is left untouched. - The start instant is captured in UTC.
- The
Runnabletask'srun()method is invoked. - The end instant is captured in UTC and the execution status is set back to
READY. - The execution status and a new
pm_executionrow holding the start, end, and duration in milliseconds are persisted together.
If the task throws, the throwable is caught and logged, the execution record is still saved, and the execution status still returns to READY. A failing task therefore does not affect the scheduling of future runs.
Customisation
Assigning a Router Layout
By default, Process Manager registers its view without a parent layout. To wrap it in the application's main layout, inject the RouteConfigurer bean using its qualifier and call setViewsRouterLayout in a @PostConstruct method:
@Autowired
@Qualifier("ProcessManagerRouteConfigurer")
private RouteConfigurer routeConfigurer;
@PostConstruct
public void configure() {
routeConfigurer.setViewsRouterLayout(MainLayout.class);
}
The @Qualifier is required because multiple AppJars may contribute a bean named RouteConfigurer.
Customising the View URL
The default URL path can be overridden in application.properties:
com.appjars.processmanager.url.process=myapp/scheduled-tasks
Testing Against the Views
The process views expose stable data-testid attributes so that integration tests can select elements without depending on the DOM structure or on translated labels. The identifiers are declared as constants in com.appjars.processmanager.flow.util.TestIds and cover the grid, the filter fields and buttons, the row action controls, and both dialogs.
Selecting by these attributes is the supported way to drive the views from a test, and keeps tests unaffected by layout or wording changes.