Dynamic Forms With Camunda and Spring StateMachine
A blueprint for building adaptive and future-proof process automation systems, ensuring they can respond quickly to changing business needs.
Join the DZone community and get the full member experience.
Join For FreeIn modern process automation, flexibility and adaptability are key. Processes often require dynamic forms that can change based on user input, business rules, or external factors. Traditional approaches, where forms are hardcoded into the process definition, can be rigid and difficult to maintain.
This article presents a flexible and scalable approach to handling dynamic forms in process automation, using Camunda BPM and Spring StateMachine as the underlying engines. We’ll explore how to decouple form definitions from process logic, enabling dynamic form rendering, validation, and submission. This approach is applicable to both Camunda and Spring StateMachine, making it a versatile solution for various process automation needs.
The Problem: Static Forms in Process Automation
In traditional process automation systems, forms are often tied directly to tasks in the process definition. For example, in Camunda, the camunda:formKey
attribute is used to associate a form with a task. While this works for simple processes, it has several limitations:
- Rigidity. Forms are hardcoded into the process definition, making it difficult to modify them without redeploying the process.
- Lack of flexibility. Forms cannot easily adapt to dynamic conditions, such as user roles, input data, or business rules.
- Maintenance overhead. Changes to forms require updates to the process definition, leading to increased maintenance complexity.
To address these limitations, we often need a more dynamic and decoupled approach to form handling.
The Solution: Dynamic Forms With YAML Configuration
Our solution involves decoupling form definitions from the process logic by storing form configurations in a YAML file (or another external source). This allows forms to be dynamically served based on the current state of the process, user roles, or other runtime conditions.
Key Components
1. Process Engine
- Camunda BPM – A powerful BPMN-based process engine for complex workflows.
- Spring State Machine – A lightweight alternative for simpler state-based processes.
2. Form Configuration
Forms are defined in a YAML file, which includes fields, labels, types, validation rules, and actions.
3. Backend Service
A Spring Boot service that serves form definitions dynamically based on the current process state.
4. Frontend
A dynamic frontend (e.g., React, Angular, or plain HTML/JavaScript) that renders forms based on the configuration returned by the backend.
Implementation With Camunda BPM
The complete source code of this approach is at the camunda
branch of the spring-statemachine-webapp GitHub repository. Let's dive into it:
1. BPMN Model
The BPMN model defines the process flow without any reference to forms. For example:
<process id="loanApplicationProcess_v1" name="Loan Application Process" isExecutable="true">
<startEvent id="startEvent" name="Start"/>
<sequenceFlow id="flow1" sourceRef="startEvent" targetRef="personalInformation"/>
<userTask id="personalInformation" name="Personal Information"/>
<sequenceFlow id="flow2" sourceRef="personalInformation" targetRef="loanDetails"/>
<!-- Other steps and gateways -->
</process>
The BPMN diagram is depicted below:
2. YAML Form Configuration
Forms are defined in a YAML file, making them easy to modify and extend:
processes:
loan_application:
steps:
personalInformation:
title: "Personal Information"
fields:
- id: "firstName"
label: "First Name"
type: "text"
required: true
- id: "lastName"
label: "Last Name"
type: "text"
required: true
actions:
- id: "next"
label: "Next"
event: "STEP_ONE_SUBMIT"
3. Backend Service
The backend service (ProcessService.java
) dynamically handles the submission of steps, the persistence of form data, and of course, it serves form definitions based on the current task:
@Transactional(readOnly = true)
public Map<String, Object> getFormDefinition(String processId) {
Task task = taskService.createTaskQuery().processInstanceBusinessKey(processId).singleResult();
//...
String processType = (String) runtimeService.getVariable(task.getProcessInstanceId(), "type");
FormFieldConfig.ProcessConfig processConfig = formFieldConfig.getProcesses().get(processType);
// ...
String stepKey = task.getTaskDefinitionKey();
FormFieldConfig.StepConfig stepConfig = processConfig.getSteps().get(stepKey);
//...
Map<String, Object> result = new HashMap<>();
result.put("processId", processId);
result.put("processType", processType);
result.put("currentState", stepKey); // Using task definition key as the current state
result.put("step", stepKey);
result.put("title", stepConfig.getTitle());
result.put("fields", stepConfig.getFields());
result.put("actions", stepConfig.getActions());
List<FormData> previousData = formDataRepository.findByProcessIdAndStep(processId, stepKey);
if (!previousData.isEmpty()) {
result.put("data", previousData.get(0).getFormDataJson());
}
return result;
}
4. Frontend
The frontend dynamically renders forms based on the configuration returned by the backend:
function loadStep() {
// Fetch form configuration from the backend
$.get(`/api/process/${processId}/form`, function(data) {
$("#formContainer").empty();
if (data.currentState === "submission") {
// Fetch and render the process summary...
} else {
// Render the form dynamically
let formHtml = `<h3>${data.title}</h3><form id="stepForm">`;
// Render form fields (text, number, date, select, etc.)
data.fields.forEach(field => {
//...
});
// Render form actions (e.g., Next, Back, Submit)
formHtml += `<div>`;
data.actions.forEach(action => {
formHtml += `<button onclick="submitStep('${data.step}', '${action.event}')">${action.label}</button>`;
});
formHtml += `</div></form>`;
$("#formContainer").html(formHtml);
// Initialize date pickers and restore previous data...
}
}).fail(function() {
$("#formContainer").html(`<p>Error loading form. Please try again.</p>`);
});
}
Implementation With Spring StateMachine
The complete source code of this approach is at the main
branch of the spring-statemachine-webapp GitHub repository. There is also another flavor with Spring StateMachine persistence enabled in the branch enable-state-machine-persistence
.
1. State Machine Configuration
The state machine defines the process flow without any reference to forms:
@Configuration
@EnableStateMachineFactory
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<ProcessStates, ProcessEvents> {
//...
@Override
public void configure(StateMachineStateConfigurer<ProcessStates, ProcessEvents> states) throws Exception {
states
.withStates()
.initial(ProcessStates.PROCESS_SELECTION)
.states(EnumSet.allOf(ProcessStates.class))
.end(ProcessStates.COMPLETED)
.end(ProcessStates.ERROR);
}
@Override
public void configure(StateMachineTransitionConfigurer<ProcessStates, ProcessEvents> transitions) throws Exception {
transitions
.withExternal()
.source(ProcessStates.PROCESS_SELECTION)
.target(ProcessStates.STEP_ONE)
.event(ProcessEvents.PROCESS_SELECTED)
.action(saveProcessAction())
.and()
.withExternal()
.source(ProcessStates.STEP_ONE)
.target(ProcessStates.STEP_TWO)
.event(ProcessEvents.STEP_ONE_SUBMIT)
.and()
.withExternal()
.source(ProcessStates.STEP_TWO)
.target(ProcessStates.STEP_THREE)
.event(ProcessEvents.STEP_TWO_SUBMIT)
.and()
.withExternal()
.source(ProcessStates.STEP_THREE)
.target(ProcessStates.SUBMISSION)
.event(ProcessEvents.STEP_THREE_SUBMIT)
.and()
.withExternal()
.source(ProcessStates.SUBMISSION)
.target(ProcessStates.COMPLETED)
.event(ProcessEvents.FINAL_SUBMIT)
.and()
// Add back navigation
.withExternal()
.source(ProcessStates.STEP_TWO)
.target(ProcessStates.STEP_ONE)
.event(ProcessEvents.BACK)
.and()
.withExternal()
.source(ProcessStates.STEP_THREE)
.target(ProcessStates.STEP_TWO)
.event(ProcessEvents.BACK)
.and()
.withExternal()
.source(ProcessStates.SUBMISSION)
.target(ProcessStates.STEP_THREE)
.event(ProcessEvents.BACK)
}
//...
}
The state machine diagram is depicted below:
2. YAML Form Configuration
The same YAML file is used to define forms for both Camunda and Spring StateMachine.
3. Backend Service
The backend service (ProcessService.java
) is quite similar to the Camunda version, i.e, it has the same responsibilities and methods. The key differences here have to do with interacting with a state machine instead of a BPMN engine.
For example, when we want to get the form definitions, the approach is like the snippet below:
@Transactional(readOnly = true)
public Map<String, Object> getFormDefinition(String processId) {
StateMachineContext<ProcessStates, ProcessEvents> stateMachineContext = loadProcessContext(processId);
String processType = (String) stateMachineContext.getExtendedState().getVariables().get("type");
String stepKey = stateToStepKey(stateMachineContext.getState());
FormFieldConfig.ProcessConfig processConfig = formFieldConfig.getProcesses().get(processType);
// ...
FormFieldConfig.StepConfig stepConfig = processConfig.getSteps().get(stepKey);
//...
Map<String, Object> result = new HashMap<>();
result.put("processId", processId);
result.put("processType", processType);
result.put("currentState", stateMachineContext.getState());
result.put("step", stepKey);
result.put("title", stepConfig.getTitle());
result.put("fields", stepConfig.getFields());
result.put("actions", stepConfig.getActions());
// Add previously saved data if available
List<FormData> previousData = formDataRepository.findByProcessIdAndStep(processId, stepKey);
if (!previousData.isEmpty()) {
result.put("data", previousData.get(0).getFormDataJson());
}
return result;
}
4. Frontend
The frontend remains the same, dynamically rendering forms based on the configuration returned by the backend.
Benefits of the Dynamic Forms Approach
- Flexibility. Forms can be modified without redeploying the process definition.
- Maintainability. Form definitions are centralized in a YAML file, making them easy to update.
- Scalability. The approach works for both simple and complex processes.
- Reusability. The same form configuration can be used across multiple processes.
Conclusion
We can create flexible, maintainable, and scalable process automation systems by decoupling form definitions from process logic. This approach works seamlessly with Camunda BPM and Spring StateMachine, making it a versatile solution for a wide range of use cases, whether building complex workflows or simple state-based processes. These dynamic forms can help you adapt to changing business requirements with ease.
Opinions expressed by DZone contributors are their own.
Comments