Repository navigation
Workflow modules in Spring Boot
Everything on this page is about one question: where a marker file, a bean definition and a configuration file have to sit so that Spring Boot finds them. The workflow modules page explains what a workflow module is and why it exists.
-
Runtime considerations
What Spring Boot does not isolate for you, and the two conventions which keep modules apart. -
Defining a workflow module
The marker file, and where it belongs in a single-module and in a multi-module project. -
Publishing a workflow module: it brings its own wiring
What a module consumed by a stranger has to declare, and what the symptoms look like without it. -
Where the BPMN files live
The conventional location, and the second one searched for the application's own module. -
Configuration
The files a module ships, where they are searched, and what beats what.
Spring Boot gives workflow modules no classloader isolation, so you have to avoid classpath conflicts yourself, for classes as well as for resources.
Give each workflow module a Java package of its own for all its classes, and a subdirectory of its own 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 that reads:
- Java package
com.awesomebank.onlinebanking.loanapproval - Download guidelines in
loan-approval/downloads/guidelines.pdf - Email templates in
loan-approval/templates/confirmation-email.tpl
A workflow module is defined by a marker text file META-INF/workflow-module containing the workflow module ID.
Where that file belongs depends on your project setup:
-
In a single-module project, create the file in
src/main/resources/META-INF. -
In a multi-module project which represents a single use case, place the file in
src/main/resources/META-INFof the main Spring Boot application module. -
In a multi-module project which represents several use cases, place one file per use case in
src/main/resources/META-INFof each use case module, which marks it as a workflow module of its own.
Examples: Microservice for online banking processes
- Project name
online-banking - Single-module project:
pom.xml src/ main/ resources/ META-INF/ workflow-module -> with the text 'online-banking' in it ... - Multi-module project having 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/ ... ... - Multi-module project having 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/ ...
A Spring Boot application scans its own package and everything below it. As long as the application sits above the workflow module in the package tree, its beans, entities and repositories are found by accident of that layout. A module which somebody else consumes does not sit there: it is a JAR from a registry, written by another team, in a package the application never heard of.
Such a module says what it contributes, once, and every application using it is spared the knowledge:
@AutoConfiguration
@ComponentScan
@EntityScan
@EnableJpaRepositories(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class)
@EnableConfigurationProperties(LoanApprovalProperties.class)
public class LoanApprovalAutoConfiguration {
}registered in a file Spring Boot reads from every JAR on the classpath:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
-> blueprint.workflowmodule.loanapproval.LoanApprovalAutoConfiguration
Together with META-INF/workflow-module this is the second and last resource of a module which does not live below its own directory, and both locations are prescribed rather than chosen.
What the symptoms look like without it. A workflow service is found because it is a bean (why), so a module nothing scans contributes none. Its BPMN is deployed nevertheless, since the module descriptor and the BPMN files are read from the classpath, and the application starts with a WARN per workflow module naming the BPMN processes nothing serves, which is what such a process costs. The report reads like a process somebody else owns while the real cause is a module nobody scanned, so it is worth reading whenever a workflow of that module does nothing. It names the classes it found, too: a boot which reports an unclaimed process reads the class files of that module and names every class carrying @WorkflowService for the process which never became a bean, with the ways to make it one - which is what turns my workflow service does nothing into a line you can act on. An aggregate whose entity is not scanned fails later still, as Not a managed type, and 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 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 naming the module's packages. It works, and it means the application knows things about a module which it should not - every consumer of 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) and, where it owns tables, its own schema management, initialized by that same auto-configuration.
Unless configured otherwise, the BPMN files of a workflow module are read from the conventional location classpath*:<workflow-module-id>/processes/<adapter-id>. Where the application's own artifact declares the single workflow module, classpath*:processes/<adapter-id> is searched as well, whichever of the two holds files. See the convention and how to deviate from it.
As described in Workflow modules, properties specific to a workflow module belong in separate files named after the workflow module ID:
loan-approval.propertiesloan-approval-prod.propertiesloan-approval.yamlloan-approval-prod.yaml
Those files behave exactly like standard application property files (see Spring Boot docs). Both .properties and .yaml are supported.
One thing they cannot do: multi-document YAML using spring.config.activate.on-profile is not supported inside workflow module config files. Put profile-specific properties into files carrying the profile as a file-name suffix instead, loan-approval-prod.yaml for example.
Configuration files are searched in several classpath locations. In a single-module project the classpath root (i.e. src/main/resources/) is sufficient. In multi-module projects put the file into a subdirectory named after the workflow module ID, the same convention as for all other workflow module resources, which keeps the modules free of classpath conflicts.
The following locations are searched (shown for loan-approval.yaml):
-
loan-approval.yaml— classpath root -
config/loan-approval.yaml— config directory -
loan-approval/loan-approval.yaml— workflow module subdirectory -
loan-approval/config/loan-approval.yaml— config inside workflow module subdirectory
The same applies to .properties files and profile-specific variants.
Those four are styles and none of them ranks above another, so the same file may lie in
exactly one of them. A workflow module which ships loan-approval.yaml at the classpath root
and in config ends the boot, with a message naming both places and asking for one of them to
go. A ranking would leave you with 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 whichever platform it runs on.
A profile-specific file is read here on its own, loan-approval-prod.yaml without
loan-approval.yaml included. Quarkus is stricter and reads it only where the file without the
profile lies next to it, so a workflow module meant for both platforms ships that file as well,
and an empty one is enough. The boot names a file of yours which a Quarkus application would
skip, see workflow modules in Quarkus.
Within the workflow module those properties are read like any other, and a workflow module ships defaults: whatever the application configures wins, whichever file it uses. This is the resulting order (highest priority first):
- system properties (
-Dloan-approval.risk.threshold=…) - environment variables
- everything the application configures, in Spring Boot's own order: its
application.*files on the classpath and next to the runner, locations added withspring.config.importorspring.config.additional-location, anddefaultProperties loan-approval-prod.yamlloan-approval-prod.propertiesloan-approval.yamlloan-approval.properties
The property sources of a workflow module are appended at the end of the environment, which is why the middle line can stay that vague: the module does not have to know what the application brings, it loses against all of it, including sources added by a starter or another EnvironmentPostProcessor. Both directions of that 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 doesVersion 1 answered this the same way; earlier snapshots of Version 2 did not. An application coming from version 1 finds the detail under Migrating from Version 1.
Level resolution of adapter properties is a different question and unaffected: vanillabp.workflow-modules.loan-approval.adapters.camunda8.* beats vanillabp.adapters.camunda8.* because it is the more specific key, no matter which file either of them stands in. Precedence decides who wins for the same key; specificity decides between different ones. A module which writes its own vanillabp.workflow-modules.<its id>.* therefore still beats an adapter-wide value of the application, and the application takes that back by writing the module-level key itself.
