Repository navigation
Workflow modules in Quarkus
This page answers one question. Where must a marker file, an index and a configuration file sit, so that Quarkus finds them? The workflow modules page explains what a workflow module is and why it exists.
-
Runtime considerations
What Quarkus 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. -
Provide index for workflow modules
Why a sub-module needs a Jandex index, and what it needs one for. -
A shared base class does not move your workflow module
Which class decides the module of a workflow service, and what the index has to do with it. -
A workflow service which is no bean stops the build
Why on this platform you find a forgotten@ApplicationScopedduring the build, not during the start. -
Publishing a workflow module
What a module used by another team has to bring (not much), and what still collides. -
Where the BPMN files live
The usual location, and why the module's own test needs no property for it. -
Configuration
The files a module ships, where they are searched, and the ordinals which put them in order.
Quarkus 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 Quarkus 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 -> add 'jandex-maven-plugin' - see next heading src/ main/ java/ ... loan-repayment/ -> use case 'loan repayment' pom.xml -> add 'jandex-maven-plugin' - see next heading 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 -> add 'jandex-maven-plugin' - see next heading 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 -> add 'jandex-maven-plugin' - see next heading src/ main/ resources/ META-INF/ workflow-module -> with the text 'online-banking-loan-repayment' in it java/ ...
In a Maven or Gradle multi-module setup, a workflow module is usually a separate sub-module. So
quarkus-maven-plugin does not process it. Build a Jandex index for the sub-module. A Jandex index is
a file which lists the classes of a JAR and their annotations. With it, quarkus-maven-plugin (run by
the main module) learns about the classes the sub-module provides.
You need the Jandex index only to discover classes inside sub-modules. That is classes annotated with
@WorkflowService, and other classes like AggregatePersistenceAware implementations and the resolver
beans named by @MultiInstanceElement(resolverBean = ...). You do not need it to detect the workflow
module itself. The META-INF/workflow-module file is registered as a marker of an application archive.
So sub-modules which contain only the descriptor and resources (for example BPMN files and
configuration) are detected even without an index. This also matters for Gradle users, who cannot use
the jandex-maven-plugin. They need to provide an index only if the sub-module contains classes to be
discovered, for example with the Jandex Gradle plugin.
Add this to your sub-module:
<build>
<plugins>
<plugin>
<groupId>io.smallrye</groupId>
<artifactId>jandex-maven-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<!-- every sub-module has to build its own index -->
<id>make-index-to-be-processed-by-quarkus-in-main-module</id>
<goals>
<goal>jandex</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>With the plugin in place, the sub-module needs only the dependencies the compiler needs. For VanillaBP that is this one, next to the own dependencies of your workflow module:
<dependencies>
<dependency>
<groupId>io.vanillabp</groupId>
<artifactId>vanillabp-quarkus-support</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>The dependency brings into your project:
- The VanillaBP SPI (service provider interface)
- The Jakarta enterprise CDI-API
- Helper classes used only during development, for the best developer experience
A workflow service belongs to the workflow module of the archive which holds its class. That class is
the one you wrote @WorkflowService onto, or the one which extends such a class. Say a library ships an
annotated base class for several applications to build on. This does not pull the workflow services of
those applications into the module of the library. Each subclass stays in the JAR it was compiled
into, and the META-INF/workflow-module of that JAR decides. The build sorts this out itself. The
Jandex index reports the class an annotation sits on and knows nothing about @Inherited.
(why)
This depends on the index. If a sub-module has no index, the build never sees its subclass. The abstract base class is then a declaration which nobody serves, and the build says exactly that.
Quarkus knows its set of beans during the build. That is why it can be stricter here than a warning.
Every class with @WorkflowService must be matched by a bean of that type or of a subtype. If there is
none, the build fails and names the class. The cause is a missing @ApplicationScoped, or a class
which the index reports but which nothing ever produces. None of this reaches a start. So on this
platform you never get a workflow service which does nothing.
Spring Boot cannot answer the same question during the build, so it answers it during the start. There, the refusal of a process nobody claims names the class. Both messages point at the same section. The only difference is when you see them.
A module which somebody else uses needs nothing beyond the index above. Its classes become visible to the build of the application. CDI resolves beans by type, so two modules never fight over a bean name. What does collide when two modules meet in one application is everything named outside the type system:
- JPA entity names (
@Entity(name = "LoanRepayment"), because one persistence unit cannot hold two entities calledAggregate) - HTTP paths
- the identifiers the BPMS sees (name clash avoidance)
The same section for the other platform is Publishing a workflow module.
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.
That second location matters for testing. Say you test a workflow module inside its own Maven module.
For that test, the module is the root application archive. So it counts as "the application IS the
workflow module". But its BPMN files sit below the module ID, where the packaged application needs
them. Both locations are searched, so the module's own test needs no resources-location property.
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
Quarkus docs). .properties works out of the box.
.yaml works once you add the Quarkus extension quarkus-config-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.
A profile-specific file needs the file without the profile next to it. Quarkus reads
loan-approval-prod.yaml only if loan-approval.yaml lies in the same directory of the same jar. So if
a workflow module ships only the first file, nobody reads its settings. The application names such a
file when it starts. An empty loan-approval.yaml next to it is enough to fix it. Spring Boot reads
the profile file on its own. So a module built there can arrive here without the second file.
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. Quarkus orders its config sources by a number called ordinal. The files of the module sit below every file of the application (highest priority first):
| Source | Ordinal |
|---|---|
system properties (-Dloan-approval.risk.threshold=…) |
400 |
| environment variables | 300 |
.env next to the runner |
295 |
config/application.yaml next to the runner |
265 |
config/application.properties next to the runner |
260 |
application.yaml on the classpath |
255 |
application.properties on the classpath |
250 |
loan-approval.yaml on the classpath |
235 |
loan-approval.properties on the classpath |
230 |
META-INF/microprofile-config.properties |
100 |
Profiles do not add rows. Quarkus loads name-prod.ext one step above name.ext. So
loan-approval-prod.yaml beats loan-approval.yaml, and application-prod.yaml beats
application.yaml, each within its own band. The gap between 235 and 250 keeps the two bands apart,
however many profiles are active.
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 doesEarlier snapshots of Version 2 used 256 and 251. That is one step above the classpath files of the application. Spring Boot handled the question in yet another way. Now both platforms agree.
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.
