Repository navigation
Workflow modules in Spring Boot
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.
-
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 used by another team has to declare, and what goes wrong without it. -
Where the BPMN files live
The usual location, and the second one which is searched for the application's own module. -
Configuration
The files a module ships, where they are searched, and which one wins.
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:
- Java package
com.awesomebank.onlinebanking.loanapproval - Download guidelines in
loan-approval/downloads/guidelines.pdf - Email templates in
loan-approval/templates/confirmation-email.tpl
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:
-
In a single-module project, create the file in
src/main/resources/META-INF. -
In a multi-module project for a single use case, put the file in
src/main/resources/META-INFof the main Spring Boot application module. -
In a multi-module project for several use cases, put one file per use case in
src/main/resources/META-INFof each use case module. This makes each of them 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 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/ ... ... - 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/ ...
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.
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.
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.propertiesloan-approval-prod.propertiesloan-approval.yamlloan-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.
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):
-
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 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.
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):
- 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 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 doesVersion 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.
