Backend Development 26 min read

Designing and Implementing a Workflow Approval System with Activiti

This article explains how to design a multi‑level workflow approval system using the Activiti engine, covering process diagram creation, deployment, task handling, variable management, exclusive gateways, and complete project configuration with Spring Boot and an H2 database.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Designing and Implementing a Workflow Approval System with Activiti

First draw a process diagram

Convert the requirement described at the beginning of the article into an Activiti BPMN diagram. Use the Idea actiBPM plugin to create a file named apply.bpmn that implements a two‑level leave‑approval process.

First‑level supervisor approval

If the leave exceeds three days, second‑level supervisor approval.

Deploy the process diagram

// Create the process engine
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();

// RepositoryService is used to deploy the diagram
RepositoryService repositoryService = engine.getRepositoryService();

// Deploy the leave‑process diagram
repositoryService.createDeployment().addClasspathResource("processes/apply.bpmn").deploy();

Deploying the diagram is usually done in the backend system; after the diagram is uploaded, no code changes are required.

Employee zhang3 starts a new process and sets approvers

Map
variableMap = new HashMap<>();
variableMap.put("applyUser", "zhang3");
variableMap.put("supervisor", "li4");
variableMap.put("upperSupervisor", "wang5");

The variables applyUser , supervisor and upperSupervisor are defined in the BPMN file as expression placeholders (e.g., ${applyUser} ) for the user‑task assignee.

Start a new process instance

ProcessInstance instance = runtimeService.startProcessInstanceByKey("apply_processor_1", variableMap);

The key apply_processor_1 is defined in the BPMN diagram and identifies the process template.

First‑level supervisor approval task

Task secondTask = taskService.createTaskQuery().taskAssignee("li4").singleResult();

taskService.setVariable(secondTask.getId(), "result1", true);
log.warn("Task completed, processId:{}, taskName:{}, taskId:{}, assignee:{}",
    secondTask.getProcessInstanceId(), secondTask.getName(), secondTask.getId(), secondTask.getAssignee());

taskService.complete(secondTask.getId());

The variable result1 is later used by an exclusive gateway to decide which branch to follow.

Second‑level supervisor approval task

Task thirdTask = taskService.createTaskQuery().taskAssignee("wang5").singleResult();
if (thirdTask != null) {
    taskService.setVariable(thirdTask.getId(), "result2", true);
    log.warn("Task completed, processId:{}, taskName:{}, taskId:{}, assignee:{}",
        thirdTask.getProcessInstanceId(), thirdTask.getName(), thirdTask.getId(), thirdTask.getAssignee(), thirdTask.getDelegationState());
    taskService.complete(thirdTask.getId());
} else {
    log.warn("No second‑level approval task found");
}

After the second‑level approval, the engine continues to the end events based on the values of result2 .

Viewing the complete execution history

List
activityInstanceList =
    historyService.createHistoricActivityInstanceQuery()
        .processInstanceId(instance.getId())
        .list();
for (HistoricActivityInstance historicActivityInstance : activityInstanceList) {
    log.warn("activityName:{}, activityType:{}, assignee:{}, taskId:{}",
        historicActivityInstance.getActivityName(),
        historicActivityInstance.getActivityType(),
        historicActivityInstance.getAssignee(),
        historicActivityInstance.getTaskId());
}

Activiti project basic configuration

Add Activiti dependency to pom.xml

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-spring-boot-starter-basic</artifactId>
    <version>5.23.0</version>
</dependency>

Configure data source and engine bean

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="org.h2.Driver"/>
        <property name="url" value="jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;mode=MySQL;"/>
    </bean>

    <bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
        <property name="dataSource" ref="dataSource"/>
        <property name="databaseSchemaUpdate" value="true"/>
    </bean>

    <context:component-scan base-package="com.muppet.activiti"/>
</beans>

Full example test class

@SpringBootTest(classes = {ActivitiStartApplication.class})
class ActivitiStartApplicationTests {
    private static final Logger log = LoggerFactory.getLogger(ActivitiStartApplicationTests.class);

    @Autowired
    private ApplyTaskListener applyTaskListener;

    @Test
    void contextLoads() {
        System.out.println("Startup successful");
        ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
        RepositoryService repositoryService = engine.getRepositoryService();
        RuntimeService runtimeService = engine.getRuntimeService();
        TaskService taskService = engine.getTaskService();
        HistoryService historyService = engine.getHistoryService();
        repositoryService.createDeployment().addClasspathResource("processes/apply.bpmn").deploy();

        Map
variableMap = new HashMap<>();
        variableMap.put("applyUser", "zhang3");
        variableMap.put("supervisor", "li4");
        variableMap.put("upperSupervisor", "wang5");

        ProcessInstance instance = runtimeService.startProcessInstanceByKey("apply_processor_1", variableMap);

        Task firstTask = taskService.createTaskQuery().taskAssignee("zhang3").singleResult();
        log.warn("Task completed, processId:{}, taskName:{}, taskId:{}, assignee:{}",
                firstTask.getProcessInstanceId(), firstTask.getName(), firstTask.getId(), firstTask.getAssignee());
        taskService.complete(firstTask.getId(), Maps.newHashMap("day", 4));

        // ... (the rest of the code follows the same pattern as described above)
    }
}

Activiti workflow engine database design

Activiti stores process definitions, runtime data, and historical records in a series of tables:

ACT_RE_* : process definitions and static resources.

ACT_RU_* : runtime data such as tasks, process instances, variables, and executions.

ACT_HI_* : historical data for audit and analysis after a process ends.

ACT_ID_* : user, group, and permission information.

Other auxiliary tables for event logs, jobs, timers, etc.

Workflow engine API overview

ProcessEngine : entry point to obtain various service APIs.

RepositoryService : manage process definitions (deployment, deletion).

RuntimeService : manage process instances (start, suspend, delete).

TaskService : manage tasks (create, complete, query).

HistoryService : query historical information (completed tasks, instances, variables).

What is a BPMN diagram

BPMN (Business Process Modeling Notation) is a standard language for modeling business processes, making them understandable across departments.

Activiti is an open‑source BPM engine based on the experience of the original jBPM project.

How to install the Idea actiBPM plugin

Visit the plugin page for installation instructions.

Example of a second‑level supervisor user task in BPMN XML

<userTask activiti:assignee="${upperSupervisor}" activiti:async="false" activiti:exclusive="true" id="_10" name="Second‑level supervisor approval"/>

Complete BPMN XML for the leave‑approval process

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" ...>
    <process id="apply_processor_1" isClosed="false" isExecutable="true">
        <startEvent id="_4" name="Start"/>
        <userTask id="_5" name="Leave request" activiti:assignee="${applyUser}"/>
        <userTask id="_6" name="Supervisor approval" activiti:assignee="${supervisor}"/>
        <exclusiveGateway id="_7" name="First‑level result"/>
        <endEvent id="_8" name="Rejected"/>
        <exclusiveGateway id="_9" name="Day check"/>
        <userTask id="_10" name="Second‑level supervisor approval" activiti:assignee="${upperSupervisor}"/>
        <endEvent id="_11" name="Approved"/>
        ... (sequence flows with condition expressions using #{result1==true}, #{day>3}, etc.)
    </process>
    ... (BPMN diagram graphics)
</definitions>

Comparison with similar workflow engines

Camunda and Flowable are both derived from Activiti and share similar usage patterns.

Five‑step workflow implementation

Define the BPMN diagram using a modeling tool.

Deploy the diagram to the workflow engine.

Start a process instance based on the deployed template.

Execute tasks; the engine creates tasks and assigns them to the appropriate users.

Register listeners to capture events such as task completion or process end.

Use the provided query and monitoring APIs to view the status and history of process instances.

Final note (please follow)

The author has compiled three technical columns into PDF files. To obtain them, follow the public account "Code Monkey Technical Column" and reply with the corresponding keywords:

Spring Cloud Advanced – reply "Spring Cloud 进阶".

Spring Boot Advanced – reply "Spring Boot进阶".

MyBatis Advanced – reply "Mybatis 进阶".

After following, you can also join the discussion group by replying "加群".

JavaworkflowBPMNSpring BootActivitiprocess-engine
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.