Build a Leave Approval Workflow in Spring Boot with Flowable Using Three Annotations
This article shows how to replace hard‑coded if‑else approval logic with Flowable workflow in a Spring Boot 3.x project, using the three core annotations @EnableProcessApplication, @Deployment and @ProcessVariable to auto‑configure the engine, deploy BPMN files, inject variables, and implement a complete leave‑request service.
Why a Workflow Engine?
Typical approval features such as leave, reimbursement, procurement, and onboarding are often implemented with hard‑coded if‑else branches, which become a maintenance nightmare when new nodes are added or business rules change. Introducing a workflow engine decouples process logic from business code, enabling visual configuration, versioning, and audit trails.
Three Core Annotations
@EnableProcessApplication : Enables automatic configuration of the Flowable engine, creating all required beans and database tables without manual setup.
@Deployment : Deploys BPMN process definitions at application startup, handling versioning and allowing seamless upgrades.
@ProcessVariable : Injects process variables directly into Spring beans or method parameters, eliminating manual calls to DelegateExecution.getVariable.
Step 1 – Add Maven Dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Flowable Spring Boot starter -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>7.0.1</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>Step 2 – Basic Configuration (application.yml)
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/flowable_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
flowable:
# Auto‑create/upgrade tables on startup
database-schema-update: true
# Store activity‑level history for full audit
history-level: activity
# Auto‑deploy BPMN files under classpath:/processes/
process-definition-location-prefix: classpath*:/processes/When the application starts, Flowable creates four groups of tables (ACT_RE_*, ACT_RU_*, ACT_HI_*, ACT_GE_*) to store definitions, runtime data, history, and engine configuration.
Annotation 1 – @EnableProcessApplication
Adding this annotation to the Spring Boot main class triggers full engine initialization.
import org.flowable.spring.boot.EnableProcessApplication;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableProcessApplication
public class FlowableLeaveApplication {
public static void main(String[] args) {
SpringApplication.run(FlowableLeaveApplication.class, args);
}
}The annotation makes beans such as RuntimeService, TaskService, HistoryService and RepositoryService available for injection, and Spring’s transaction manager synchronizes database and workflow transactions automatically.
Annotation 2 – @Deployment
Design a simple leave‑approval BPMN file (e.g., leave.bpmn20.xml) with an exclusive gateway that routes based on the number of leave days.
<exclusiveGateway id="gatewayDays" name="天数判断"/>
<sequenceFlow sourceRef="gatewayDays" targetRef="directorApprove">
<conditionExpression xsi:type="tFormalExpression">${days > 3}</conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="gatewayDays" targetRef="hrRecord">
<conditionExpression xsi:type="tFormalExpression">${days <= 3}</conditionExpression>
</sequenceFlow>Placing the file under resources/processes/ and annotating a configuration class with @Deployment automatically loads it at startup.
import org.flowable.spring.boot.Deployment;
import org.springframework.context.annotation.Configuration;
@Configuration
@Deployment(resources = "processes/leave.bpmn20.xml")
public class FlowableDeployConfig {
}Flowable creates a new version each time the BPMN file changes; running instances continue with the old version while new instances use the latest.
Annotation 3 – @ProcessVariable
Process variables drive branch logic. Using @ProcessVariable injects them directly into a delegate bean.
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;
import org.flowable.engine.impl.delegate.@ProcessVariable;
import org.springframework.stereotype.Component;
@Component
public class HrRecordDelegate implements JavaDelegate {
// Automatically injected variables
@ProcessVariable
private Integer days;
@ProcessVariable
private String userId;
@Override
public void execute(DelegateExecution execution) {
System.out.println("HR record: user " + userId + " leave " + days + " days");
// Business logic such as updating tables, sending notifications, etc.
}
}Reference this delegate in the BPMN service‑task node; the engine supplies the variables when the task is executed.
Complete Business Implementation
DTO
@Data
public class LeaveApplyDTO {
// Applicant ID
private String userId;
// Number of leave days
private Integer days;
// Reason for leave
private String reason;
}Service Layer
@Service
public class LeaveProcessService {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private HistoryService historyService;
private static final String PROCESS_KEY = "leave-process";
/** 1. Start a leave request */
public String startProcess(LeaveApplyDTO dto) {
Map<String, Object> variables = new HashMap<>();
variables.put("userId", dto.getUserId());
variables.put("days", dto.getDays());
variables.put("reason", dto.getReason());
// Example manager ID
variables.put("deptManager", "manager001");
ProcessInstance instance = runtimeService.startProcessInstanceByKey(
PROCESS_KEY,
dto.getUserId(), // business key
variables);
return instance.getId();
}
/** 2. Query my pending tasks */
public List<TaskVO> getMyTodoTasks(String userId) {
List<Task> tasks = taskService.createTaskQuery()
.taskAssignee(userId)
.orderByTaskCreateTime().desc()
.list();
return tasks.stream().map(task -> {
TaskVO vo = new TaskVO();
vo.setTaskId(task.getId());
vo.setTaskName(task.getName());
vo.setProcessInstanceId(task.getProcessInstanceId());
return vo;
}).collect(Collectors.toList());
}
/** 3. Approve or reject a task */
public void approveTask(String taskId, boolean pass, String comment) {
Map<String, Object> variables = new HashMap<>();
variables.put("approvePass", pass);
variables.put("comment", comment);
if (pass) {
taskService.complete(taskId, variables);
} else {
String procInstId = taskService.createTaskQuery().taskId(taskId).singleResult().getProcessInstanceId();
runtimeService.deleteProcessInstance(procInstId, "Rejected: " + comment);
}
}
/** 4. Retrieve approval history */
public List<HistoryVO> getProcessHistory(String processInstanceId) {
List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(processInstanceId)
.orderByHistoricTaskInstanceEndTime().asc()
.list();
return list.stream().map(his -> {
HistoryVO vo = new HistoryVO();
vo.setTaskName(his.getName());
vo.setAssignee(his.getAssignee());
vo.setStartTime(his.getStartTime());
vo.setEndTime(his.getEndTime());
return vo;
}).collect(Collectors.toList());
}
}Controller
@RestController
@RequestMapping("/leave")
public class LeaveController {
@Autowired
private LeaveProcessService leaveProcessService;
@PostMapping("/apply")
public Result<String> apply(@RequestBody LeaveApplyDTO dto) {
String processId = leaveProcessService.startProcess(dto);
return Result.success(processId);
}
@GetMapping("/todo")
public Result<List<TaskVO>> todo(@RequestParam String userId) {
return Result.success(leaveProcessService.getMyTodoTasks(userId));
}
@PostMapping("/approve")
public Result<?> approve(@RequestParam String taskId,
@RequestParam boolean pass,
@RequestParam String comment) {
leaveProcessService.approveTask(taskId, pass, comment);
return Result.success("Approval completed");
}
@GetMapping("/history")
public Result<List<HistoryVO>> history(@RequestParam String processInstanceId) {
return Result.success(leaveProcessService.getProcessHistory(processInstanceId));
}
}Practical Tips & Pitfalls
Transaction sync : If you create a manual transaction outside Spring, the workflow transaction will not roll back together. Annotate service methods with @Transactional to keep DB and Flowable actions atomic.
Process variable size : Store only keys, status flags, or small decision parameters in variables. Keep full business data in regular tables and link via a business key.
History table growth : Set an appropriate history-level (e.g., activity), archive records older than three months, and query runtime data from ACT_RU_* tables only.
Exclusive gateway null‑pointer : Ensure variables like days have default values or add a fallback flow to avoid exceptions when the variable is missing.
Process upgrades : Flowable supports multiple versions; do not delete old definitions. New instances use the latest version while running instances continue with the version they started with.
Conclusion
By adding only three annotations— @EnableProcessApplication, @Deployment and @ProcessVariable —a Spring Boot project gains full workflow capabilities: automatic engine setup, visual process deployment, variable‑driven branching, versioned definitions, and complete audit trails. Compared with hard‑coded if‑else logic, this approach dramatically improves maintainability, scalability, and flexibility for approval scenarios, even in small‑to‑medium projects.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
