Skip to content

Workflow modules in Spring Boot

Stephan Pelikan edited this page Oct 12, 2026 · 21 revisions

This page answers one question. Where must a marker file, a bean definition and a configuration file sit, so that Spring Boot finds them? The workflow modules page explains what a workflow module is and why it exists.

  1. Runtime considerations
    What Spring Boot does not isolate for you, and the two conventions which keep modules apart.
  2. Defining a workflow module
    The marker file, and where it belongs in a single-module and in a multi-module project.
  3. Publishing a workflow module: it brings its own wiring
    What a module used by another team has to declare, and what goes wrong without it.
  4. Where the BPMN files live
    The usual location, and the second one which is searched for the application's own module.
  5. Configuration
    The files a module ships, where they are searched, and which one wins.

Runtime considerations

Classloader isolation

Spring Boot does not isolate workflow modules in classloaders of their own. So you have to avoid classpath conflicts yourself, for classes and for resources.

Give each workflow module its own Java package for all its classes. Give it its own subdirectory for all its resources. By convention, that subdirectory is named after the workflow module ID. For the loan approval module of an online banking application this looks like:

  1. Java package com.awesomebank.onlinebanking.loanapproval
  2. Download guidelines in loan-approval/downloads/guidelines.pdf
  3. Email templates in loan-approval/templates/confirmation-email.tpl

Defining a workflow module

A marker text file META-INF/workflow-module defines a workflow module. It contains the workflow module ID.

Where that file belongs depends on your project setup:

  1. In a single-module project, create the file in src/main/resources/META-INF.

  2. In a multi-module project for a single use case, put the file in src/main/resources/META-INF of the main Spring Boot application module.

  3. In a multi-module project for several use cases, put one file per use case in src/main/resources/META-INF of each use case module. This makes each of them a workflow module of its own.

Examples: Microservice for online banking processes

  1. Project name online-banking
  2. Single-module project:
    pom.xml
    src/
      main/
        resources/
          META-INF/
            workflow-module    -> with the text 'online-banking' in it
    ...
    
  3. Multi-module project with all processes in one workflow module:
    pom.xml
    main-application/            -> main application
      pom.xml                    -> having dependencies to 'loan-approval' and 'loan-repayment'
      src/
        main/
          resources/
            META-INF/
              workflow-module    -> with the text 'online-banking' in it
    loan-approval/               -> use case 'loan approval'
      pom.xml
      src/
        main/
          java/
             ...
    loan-repayment/              -> use case 'loan repayment'
      pom.xml
      src/
        main/
          java/
             ...
    ...
    
  4. Multi-module project with one workflow module per use case:
    pom.xml
    main-application/            -> main application
      pom.xml                    -> having dependencies to 'loan-approval' and 'loan-repayment'
       src/
        main/
          java/
             ...
    loan-approval/               -> use case 'loan approval'
      pom.xml
      src/
        main/
          resources/
            META-INF/
              workflow-module    -> with the text 'online-banking-loan-approval' in it
          java/
             ...
    loan-repayment/              -> use case 'loan repayment'
      pom.xml
      src/
        main/
          resources/
            META-INF/
              workflow-module    -> with the text 'online-banking-loan-repayment' in it
          java/
             ...
    

Publishing a workflow module: it brings its own wiring

A Spring Boot application scans its own package and everything below it. Often the application sits above the workflow module in the package tree. Then the beans, entities and repositories of the module are found, but only by luck of that layout. A module which somebody else uses does not sit there. It is a JAR from a registry, written by another team, in a package the application has never heard of.

Such a module says once what it contributes. Then no application which uses it needs to know:

@AutoConfiguration
@ComponentScan
@EntityScan
@EnableJpaRepositories(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class)
@EnableConfigurationProperties(LoanApprovalProperties.class)
public class LoanApprovalAutoConfiguration {
}

You register this class in a file which Spring Boot reads from every JAR on the classpath:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  -> blueprint.workflowmodule.loanapproval.LoanApprovalAutoConfiguration

META-INF/workflow-module and this file are the only two resources of a module which do not live below the module's own directory. Their locations are fixed, you cannot choose them.

What the symptoms look like without it. A workflow service is found because it is a bean (why). So a module which nothing scans contributes no workflow service. Its BPMN is deployed anyway, because the module descriptor and the BPMN files are read from the classpath. The start then ends and names the BPMN processes which nothing serves, see a process somebody else serves. The message reads as if somebody else owned the process. But the real cause is a module which nobody scanned. So check this first, before you silence the message with implemented-externally. The message also names the classes it found. If a start refuses a process nobody claims, it reads the class files of that module. It names every class with @WorkflowService for the process which never became a bean, and the ways to make it one. This turns my workflow service does nothing into a line you can act on. If the entity of an aggregate is not scanned, it fails later, as Not a managed type. A repository which was never created fails as a missing bean of that type.

What has to be renamed once two modules meet in one application, none of which the compiler mentions:

What collides What it looks like What to do
bean names conflicts with existing, non-compatible bean definition of same name name the beans: @RestController("loanApprovalApiController"), and the same for the service and the other classes which a module has once per use case
repository beans BeanDefinitionOverrideException for aggregateRepository @EnableJpaRepositories(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class), which names them after their class
JPA entities Duplicate entity name @Entity(name = "LoanRepayment"), because one persistence unit cannot hold two entities called Aggregate
HTTP paths the second mapping wins or the boot fails one path prefix per module
identifiers inside the BPMS nothing, until two workflows are mixed up name clash avoidance

The fallback, for a module which brings no auto-configuration: the application declares @ComponentScan, @EntityScan and @EnableJpaRepositories with the packages of the module. This works. But then the application knows things about the module which it should not know, and every application using that module has to know them again.

What else belongs into a published module: its configuration per environment (<module-id>-<profile>.yaml, see Configuration below). If the module owns tables, it also brings its own schema management, set up by that same auto-configuration.

Where the BPMN files live

Unless you configure something else, the BPMN files of a workflow module are read from the usual location classpath*:<workflow-module-id>/processes/<adapter-id>. If the application's own artifact declares the single workflow module, classpath*:processes/<adapter-id> is searched as well. It does not matter which of the two holds the files. See the convention and how to deviate from it.

Configuration

As described in Workflow modules, properties specific to a workflow module belong in separate files. These files are named after the workflow module ID:

  • loan-approval.properties
  • loan-approval-prod.properties
  • loan-approval.yaml
  • loan-approval-prod.yaml

These files behave exactly like normal application property files (see Spring Boot docs). Both .properties and .yaml are supported.

There is one thing they cannot do. Workflow module config files do not support multi-document YAML with spring.config.activate.on-profile. Put profile-specific properties into files with the profile as a file name suffix instead, for example loan-approval-prod.yaml.

File locations

Configuration files are searched in several classpath locations. In a single-module project the classpath root (that is, src/main/resources/) is enough. In a multi-module project, put the file into a subdirectory named after the workflow module ID. This is the same convention as for all other resources of a workflow module, and it keeps the modules free of classpath conflicts.

These locations are searched (shown for loan-approval.yaml):

  1. loan-approval.yaml — classpath root
  2. config/loan-approval.yaml — config directory
  3. loan-approval/loan-approval.yaml — workflow module subdirectory
  4. loan-approval/config/loan-approval.yaml — config inside workflow module subdirectory

The same applies to .properties files and to profile-specific variants.

These four locations are styles. None of them ranks above another. So the same file may lie in exactly one of them. Say a workflow module ships loan-approval.yaml at the classpath root and in config. Then the boot stops with a message which names both places and asks you to remove one of them. With a ranking, you could have a setting whose file you cannot see from the outside.

Quarkus and Spring Boot read the same four places. So a workflow module keeps its style on either platform.

Here, a profile-specific file is read on its own. loan-approval-prod.yaml is read even without loan-approval.yaml. Quarkus is stricter. It reads the profile file only if the file without the profile lies next to it. So a workflow module meant for both platforms ships that file as well. An empty file is enough. The boot names any file of yours which a Quarkus application would skip, see workflow modules in Quarkus.

Priority

Inside the workflow module, these properties are read like any other properties. A workflow module ships defaults. Whatever the application configures wins, no matter which file it uses. This is the resulting order (highest priority first):

  1. system properties (-Dloan-approval.risk.threshold=…)
  2. environment variables
  3. everything the application configures, in Spring Boot's own order: its application.* files on the classpath and next to the runner, locations added with spring.config.import or spring.config.additional-location, and defaultProperties
  4. loan-approval-prod.yaml
  5. loan-approval-prod.properties
  6. loan-approval.yaml
  7. loan-approval.properties

The property sources of a workflow module are added at the end of the environment. That is why the middle line can stay so vague. The module does not have to know what the application brings. It loses against all of it, also against sources added by a starter or by another EnvironmentPostProcessor. Both directions of this rule are in daily use:

# application-prod.yaml of the application
urls:
  external-system: https://es.com

# loan-approval.yaml inside the module's jar
loan-approval:
  external-system:
    url: ${urls.external-system}          # resolved against the application
    timeout: PT10S                        # a default the application may override
# application-prod.yaml of the application
loan-approval:
  external-system:
    timeout: PT30S                        # ... and here it does

Version 1 handled this the same way. Earlier snapshots of Version 2 did not. If your application comes from version 1, you find the details under Migrating from Version 1.

The levels of adapter properties are a different question, and this rule does not affect them. vanillabp.workflow-modules.loan-approval.adapters.camunda8.* beats vanillabp.adapters.camunda8.* because it is the more specific key. It does not matter which file either of them stands in. Priority decides which value wins for the same key. Specificity decides between different keys. So a module which writes its own vanillabp.workflow-modules.<its id>.* still beats an adapter-wide value of the application. The application can win back by writing the module-level key itself.

Clone this wiki locally