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.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
Build a Leave Approval Workflow in Spring Boot with Flowable Using Three Annotations

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

WorkflowbpmnSpring Bootannotationsflowableapproval-process
Java Tech Workshop
Written by

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.

0 followers
Reader feedback

How this landed with the community

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.