DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Structured Logging in Spring Boot 3.4 for Improved Logs
  • External Task Client Implementation in Camunda With Spring Boot Application
  • Running Spring Boot Application With Embedded Camunda Engine
  • Docker With Spring Boot and MySQL: Docker Swarm Part 3

Trending

  • Mastering Fluent Bit: Installing and Configuring Fluent Bit on Kubernetes (Part 3)
  • What Is Plagiarism? How to Avoid It and Cite Sources
  • Apache Doris vs Elasticsearch: An In-Depth Comparative Analysis
  • Infrastructure as Code (IaC) Beyond the Basics
  1. DZone
  2. Coding
  3. Frameworks
  4. Dynamic Forms With Camunda and Spring StateMachine

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.

By 
Kyriakos Mandalas user avatar
Kyriakos Mandalas
DZone Core CORE ·
Mar. 06, 25 · Analysis
Likes (4)
Comment
Save
Tweet
Share
4.1K Views

Join the DZone community and get the full member experience.

Join For Free

In 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:

  1. Rigidity. Forms are hardcoded into the process definition, making it difficult to modify them without redeploying the process.
  2. Lack of flexibility. Forms cannot easily adapt to dynamic conditions, such as user roles, input data, or business rules.
  3. 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:

XML
 
<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:

BPMN diagram

2. YAML Form Configuration

Forms are defined in a YAML file, making them easy to modify and extend:

YAML
 
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:

Java
 
@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:

JavaScript
 
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:

Java
 
@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:

State machine diagram

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:

Java
 
@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

  1. Flexibility. Forms can be modified without redeploying the process definition.
  2. Maintainability. Form definitions are centralized in a YAML file, making them easy to update.
  3. Scalability. The approach works for both simple and complex processes.
  4. 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.

Camunda Machine Form (document) Spring Boot

Opinions expressed by DZone contributors are their own.

Related

  • Structured Logging in Spring Boot 3.4 for Improved Logs
  • External Task Client Implementation in Camunda With Spring Boot Application
  • Running Spring Boot Application With Embedded Camunda Engine
  • Docker With Spring Boot and MySQL: Docker Swarm Part 3

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!