Workflow engines automate business processes, providing a structured way to handle complex, long-running, and state-dependent logic. Java offers several powerful workflow solutions that help organizations implement business rules, approval flows, and process automation efficiently. This guide explores workflow engines in Java, with practical implementation examples and best practices.
Key benefits of using workflow engines:
Business Process Model and Notation (BPMN) is a standardized graphical notation for representing business processes. BPMN 2.0 is the latest version and is widely supported by Java workflow engines.
Element | Description |
---|---|
Events | Start, end, intermediate events that trigger actions or wait for occurrences |
Activities | Tasks, sub-processes, service tasks that define work to be performed |
Gateways | Decision points that control flow (exclusive, parallel, inclusive, event-based) |
Sequence Flows | Connections showing the order of execution |
Pools and Lanes | Visual organization of processes and responsibilities |
BPMN diagrams can be created using modeling tools such as:
The resulting diagram is typically saved as an XML file that workflow engines can interpret and execute.
Camunda is a leading open-source workflow and decision automation platform that follows the BPMN 2.0 standard. It offers comprehensive tools for process design, execution, and monitoring.
// Add Camunda dependencies to pom.xml
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter</artifactId>
<version>7.17.0</version>
</dependency>
// Spring Boot application with Camunda
@SpringBootApplication
@EnableProcessApplication
public class CamundaWorkflowApplication {
public static void main(String[] args) {
SpringApplication.run(CamundaWorkflowApplication.class, args);
}
}
// Configuration in application.properties
spring.datasource.url=jdbc:h2:mem:camunda
spring.datasource.username=sa
spring.datasource.password=password
camunda.bpm.admin-user.id=admin
camunda.bpm.admin-user.password=admin
camunda.bpm.filter.create=All tasks
// Service task implementation
@Component
@Named("loanApprovalService")
public class LoanApprovalService implements JavaDelegate {
private static final Logger LOG = LoggerFactory.getLogger(LoanApprovalService.class);
@Override
public void execute(DelegateExecution execution) throws Exception {
// Get process variables
String customerId = (String) execution.getVariable("customerId");
BigDecimal amount = (BigDecimal) execution.getVariable("amount");
// Business logic
boolean approved = evaluateLoanApplication(customerId, amount);
// Set result
execution.setVariable("approved", approved);
LOG.info("Loan application for customer {} amount {} approved: {}",
customerId, amount, approved);
}
private boolean evaluateLoanApplication(String customerId, BigDecimal amount) {
// Implement loan evaluation logic
return amount.compareTo(new BigDecimal("50000")) < 0;
}
}
// Starting a process instance
@RestController
@RequestMapping("/api/loan-application")
public class LoanApplicationController {
@Autowired
private RuntimeService runtimeService;
@PostMapping
public ResponseEntity<Map<String, Object>> startProcess(@RequestBody LoanApplicationRequest request) {
// Create variables
Map<String, Object> variables = new HashMap<>();
variables.put("customerId", request.getCustomerId());
variables.put("amount", request.getAmount());
// Start process
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(
"loan_approval_process",
request.getApplicationId(),
variables
);
// Return response
Map<String, Object> response = new HashMap<>();
response.put("processInstanceId", processInstance.getProcessInstanceId());
response.put("businessKey", processInstance.getBusinessKey());
return ResponseEntity.ok(response);
}
}
// Task service to handle user tasks
@Service
public class TaskService {
@Autowired
private org.camunda.bpm.engine.TaskService camundaTaskService;
public List<Task> getTasksForUser(String userId) {
return camundaTaskService.createTaskQuery()
.taskAssignee(userId)
.active()
.list();
}
public void completeTask(String taskId, Map<String, Object> variables) {
camundaTaskService.complete(taskId, variables);
}
public void claimTask(String taskId, String userId) {
camundaTaskService.claim(taskId, userId);
}
}
jBPM is a flexible Business Process Management (BPM) suite that's part of the KIE (Knowledge Is Everything) platform. It leverages the Eclipse BPMN 2.0 designer for modeling processes.
// Add jBPM dependencies to pom.xml
<dependency>
<groupId>org.kie</groupId>
<artifactId>kie-spring</artifactId>
<version>7.67.0.Final</version>
</dependency>
<dependency>
<groupId>org.jbpm</groupId>
<artifactId>jbpm-spring-boot-starter-basic</artifactId>
<version>7.67.0.Final</version>
</dependency>
// Spring Boot application with jBPM
@SpringBootApplication
public class JbpmApplication {
public static void main(String[] args) {
SpringApplication.run(JbpmApplication.class, args);
}
}
// Create a work item handler
@Component("EmailHandler")
public class EmailWorkItemHandler implements WorkItemHandler {
private static final Logger logger = LoggerFactory.getLogger(EmailWorkItemHandler.class);
@Override
public void executeWorkItem(WorkItem workItem, WorkItemManager manager) {
// Get parameters
String to = (String) workItem.getParameter("to");
String subject = (String) workItem.getParameter("subject");
String body = (String) workItem.getParameter("body");
// Send email (implementation omitted)
logger.info("Sending email to {} with subject: {}", to, subject);
// Complete the work item
manager.completeWorkItem(workItem.getId(), Collections.emptyMap());
}
@Override
public void abortWorkItem(WorkItem workItem, WorkItemManager manager) {
// Handle abort
logger.warn("Aborting email work item: {}", workItem.getId());
}
}
// Register work item handler
@Configuration
public class JbpmConfiguration {
@Bean
public RuntimeManager runtimeManager(RuntimeManagerFactory runtimeManagerFactory,
EntityManagerFactory entityManagerFactory,
UserGroupCallback userGroupCallback) {
RuntimeEnvironmentBuilder builder = RuntimeEnvironmentBuilder.Factory.get()
.newDefaultBuilder()
.entityManagerFactory(entityManagerFactory)
.userGroupCallback(userGroupCallback)
.addAsset(ResourceFactory.newClassPathResource("processes/approval-process.bpmn2"), ResourceType.BPMN2);
return runtimeManagerFactory.newPerRequestRuntimeManager(builder.get(), "approval-process");
}
@Bean
public WorkItemHandler emailHandler() {
return new EmailWorkItemHandler();
}
@PostConstruct
public void registerWorkItemHandlers(@Autowired RuntimeManager runtimeManager) {
RuntimeEngine engine = runtimeManager.getRuntimeEngine(EmptyContext.get());
KieSession ksession = engine.getKieSession();
ksession.getWorkItemManager().registerWorkItemHandler("Email", emailHandler());
runtimeManager.disposeRuntimeEngine(engine);
}
}
@RestController
@RequestMapping("/api/expense-claims")
public class ExpenseClaimController {
@Autowired
private RuntimeManager runtimeManager;
@PostMapping
public ResponseEntity<Map<String, Object>> submitExpenseClaim(@RequestBody ExpenseClaimRequest request) {
// Get runtime engine
RuntimeEngine engine = runtimeManager.getRuntimeEngine(EmptyContext.get());
KieSession ksession = engine.getKieSession();
try {
// Set process variables
Map<String, Object> params = new HashMap<>();
params.put("employeeId", request.getEmployeeId());
params.put("amount", request.getAmount());
params.put("description", request.getDescription());
params.put("submissionDate", new Date());
// Start process
ProcessInstance processInstance = ksession.startProcess("expense-approval", params);
// Create response
Map<String, Object> response = new HashMap<>();
response.put("processInstanceId", processInstance.getId());
response.put("status", processInstance.getState());
return ResponseEntity.ok(response);
} finally {
// Always dispose runtime engine
runtimeManager.disposeRuntimeEngine(engine);
}
}
@GetMapping("/{processInstanceId}")
public ResponseEntity<Map<String, Object>> getExpenseClaimStatus(@PathVariable long processInstanceId) {
RuntimeEngine engine = runtimeManager.getRuntimeEngine(EmptyContext.get());
try {
// Get process instance
ProcessInstance processInstance = engine.getKieSession().getProcessInstance(processInstanceId);
if (processInstance == null) {
// Check history service for completed processes
AuditService auditService = engine.getAuditService();
ProcessInstanceLog log = auditService.findProcessInstance(processInstanceId);
if (log != null) {
Map<String, Object> response = new HashMap<>();
response.put("processInstanceId", log.getProcessInstanceId());
response.put("status", log.getStatus());
response.put("startDate", log.getStart());
response.put("endDate", log.getEnd());
return ResponseEntity.ok(response);
} else {
return ResponseEntity.notFound().build();
}
} else {
Map<String, Object> response = new HashMap<>();
response.put("processInstanceId", processInstance.getId());
response.put("status", processInstance.getState());
return ResponseEntity.ok(response);
}
} finally {
runtimeManager.disposeRuntimeEngine(engine);
}
}
}
For simpler workflow needs, Spring Statemachine provides a lightweight alternative to full BPMN engines. It's ideal for state-driven applications with clear transitions.
// Add Spring Statemachine dependency
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>3.2.0</version>
</dependency>
// Define states and events
public enum OrderState {
CREATED, PAID, PREPARING, SHIPPED, DELIVERED, CANCELLED
}
public enum OrderEvent {
PAY, PREPARE, SHIP, DELIVER, CANCEL
}
@Configuration
@EnableStateMachine
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderState, OrderEvent> {
@Override
public void configure(StateMachineStateConfigurer<OrderState, OrderEvent> states) throws Exception {
states
.withStates()
.initial(OrderState.CREATED)
.state(OrderState.PAID)
.state(OrderState.PREPARING)
.state(OrderState.SHIPPED)
.end(OrderState.DELIVERED)
.end(OrderState.CANCELLED);
}
@Override
public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions) throws Exception {
transitions
.withExternal()
.source(OrderState.CREATED).target(OrderState.PAID)
.event(OrderEvent.PAY)
.guard(paymentGuard())
.action(paymentAction())
.and()
.withExternal()
.source(OrderState.PAID).target(OrderState.PREPARING)
.event(OrderEvent.PREPARE)
.and()
.withExternal()
.source(OrderState.PREPARING).target(OrderState.SHIPPED)
.event(OrderEvent.SHIP)
.and()
.withExternal()
.source(OrderState.SHIPPED).target(OrderState.DELIVERED)
.event(OrderEvent.DELIVER)
.and()
.withExternal()
.source(OrderState.CREATED).target(OrderState.CANCELLED)
.event(OrderEvent.CANCEL)
.and()
.withExternal()
.source(OrderState.PAID).target(OrderState.CANCELLED)
.event(OrderEvent.CANCEL)
.action(refundAction());
}
@Bean
public Guard<OrderState, OrderEvent> paymentGuard() {
return context -> {
// Check if payment is valid
return context.getExtendedState().getVariables()
.getOrDefault("paymentSuccessful", false);
};
}
@Bean
public Action<OrderState, OrderEvent> paymentAction() {
return context -> {
String orderId = (String) context.getExtendedState()
.getVariables().get("orderId");
System.out.println("Payment successful for order: " + orderId);
};
}
@Bean
public Action<OrderState, OrderEvent> refundAction() {
return context -> {
String orderId = (String) context.getExtendedState()
.getVariables().get("orderId");
System.out.println("Refunding payment for cancelled order: " + orderId);
};
}
}
@Service
public class OrderService {
@Autowired
private StateMachine<OrderState, OrderEvent> stateMachine;
@Autowired
private StateMachineFactory<OrderState, OrderEvent> stateMachineFactory;
public boolean processPayment(String orderId, BigDecimal amount) {
StateMachine<OrderState, OrderEvent> sm = getStateMachine(orderId);
// Set extended state variables
sm.getExtendedState().getVariables().put("orderId", orderId);
sm.getExtendedState().getVariables().put("amount", amount);
// Simulate payment
boolean paymentSuccessful = processPaymentLogic(orderId, amount);
sm.getExtendedState().getVariables().put("paymentSuccessful", paymentSuccessful);
// Send event
boolean eventAccepted = sm.sendEvent(OrderEvent.PAY);
// Persist state if needed
if (eventAccepted) {
saveStateMachineState(orderId, sm);
}
return eventAccepted && paymentSuccessful;
}
private StateMachine<OrderState, OrderEvent> getStateMachine(String orderId) {
// In a real app, retrieve persisted state or create new
StateMachine<OrderState, OrderEvent> sm = stateMachineFactory.getStateMachine(orderId);
sm.start();
return sm;
}
private boolean processPaymentLogic(String orderId, BigDecimal amount) {
// Implement payment processing
return true;
}
private void saveStateMachineState(String orderId, StateMachine<OrderState, OrderEvent> sm) {
// Persist state machine
}
}
@SpringBootTest
public class OrderProcessTest {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@MockBean
private ShippingService shippingService;
@Test
public void testOrderProcess() {
// Given
Map<String, Object> variables = new HashMap<>();
variables.put("orderId", "12345");
variables.put("amount", new BigDecimal("99.95"));
variables.put("customerEmail", "test@example.com");
// Mock shipping service behavior
when(shippingService.arrangeShipping(anyString()))
.thenReturn("TRACK-12345");
// When - start process
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(
"orderProcess", variables);
// Then - assert process is active
assertNotNull(processInstance);
assertFalse(processInstance.isEnded());
// Complete payment task
Task paymentTask = taskService.createTaskQuery()
.processInstanceId(processInstance.getId())
.taskDefinitionKey("reviewPaymentTask")
.singleResult();
assertNotNull(paymentTask);
Map<String, Object> paymentResult = new HashMap<>();
paymentResult.put("paymentApproved", true);
taskService.complete(paymentTask.getId(), paymentResult);
// Verify shipping service was called
verify(shippingService).arrangeShipping("12345");
// Process should have completed successfully
processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstance.getId())
.singleResult();
assertNull(processInstance); // Process has ended
}
}
Workflow engines provide powerful tools for implementing complex business processes in Java applications. By separating process logic from application code, they enable more maintainable, adaptable solutions that can evolve with changing business requirements.
Whether you choose Camunda, Flowable, jBPM, or Spring Statemachine depends on your specific needs, but all these platforms offer mature solutions for process automation. When implementing workflows, focus on clarity, maintainability, and proper error handling to ensure robust process execution in production environments.