Purpose
This module is a technical sample for TDD-first multi-tenant extension of a Chenile workflow service.
It shows how to:
- keep reusable core module (
vehicle) - add independent tenant extension modules (
custom-tenant0,custom-tenant1) - package both in one runtime (
packager) with multi-datasource routing - verify behavior through cucumber tests
Module path:
/ajapro/chenile-samples/how_to_extend_chenile_service_multitenant_pubsub
GitHub source:
- https://github.com/ajapros/chenile-samples/tree/main/how_to_extend_chenile_service_multitenant_pubsub
1. Module Layout
Root modules:
vehiclevehicle/api: core model contractsvehicle/service: core workflow + REST + central HTTP subtype registration
custom-tenant0api: tenant0 extension entity (tenant0_ext)service: tenant0-specificexttransition action and tests
custom-tenant1api: tenant1 extension entity (tenant1_ext)service: tenant1-specificexttransition action and tests
packagerservice: imports both tenant modules and validates both workflows in one JVM
2. Core Design
Subtype registration is centralized in:
vehicle/service/src/main/java/com/mycompany/myorg/vehicle/configuration/VehicleConfiguration.java
VehicleConfiguration implements WebMvcConfigurer and registers extension subtypes dynamically from:
chenile.http.extension-subtypes
This keeps tenant modules independent: they contribute YAML config values, not core wiring changes.
2.1 Tenant-Aware Action Discovery
Base workflow config wires transition action discovery with tenant awareness:
@Bean
STMTransitionActionResolver vehicleTransitionActionResolver(
@Qualifier("defaultvehicleSTMTransitionAction") STMTransitionAction<Vehicle> defaultSTMTransitionAction){
return new STMTransitionActionResolver("vehicle",defaultSTMTransitionAction, HeaderUtils.TENANT_ID_KEY.replace("x-",""));
}
What this does:
- prefix
vehicleenables standard transition action discovery - tenant key derives from request header
x-chenile-tenant-id - same workflow event (
ext) resolves tenant-specific beans
In this sample, extension beans are:
- tenant0:
@Bean("tenant0VehicleExt") - tenant1:
@Bean("tenant1VehicleExt")
2.2 Initial vs Extended Workflow
Core workflow (vehicle):
OPENED -> ASSIGNED(assign)ASSIGNED -> RESOLVED(resolve)ASSIGNED -> CLOSED(close)
Tenant extension workflow adds:
ASSIGNED -> EXTENSION(ext)EXTENSION -> CLOSED(close)
This enables shared base lifecycle with tenant-specific extension behavior.
2.3 Required Extension Subtype YAML
chenile:
http:
extension-subtypes:
- name: tenant0_ext
className: com.mycompany.myorg.vehicle.model.VehicleExtensionTenant0
- name: tenant1_ext
className: com.mycompany.myorg.vehicle.model.VehicleExtensionTenant1
3. Tenant Modules
Tenant0
- Entity type:
tenant0_ext - Config class:
custom-tenant0/service/src/main/java/com/mycompany/myorg/vehicle/extension/configuration/VehicleExtentionConfiguration.java
- Action class:
custom-tenant0/service/src/main/java/com/mycompany/myorg/vehicle/extension/service/cmd/Tenant0ExtVehicleAction.java
ext behavior:
- sets
newColumn - sets
tenant0WorkflowNote - publishes event to
vehicle.events.testwith tenant header propagation
Tenant1
- Entity type:
tenant1_ext - Config class:
custom-tenant1/service/src/main/java/com/mycompany/myorg/vehicle/extension/configuration/Tenant1VehicleConfiguration.java
- Action class:
custom-tenant1/service/src/main/java/com/mycompany/myorg/vehicle/extension/service/cmd/Tenant1ExtVehicleAction.java
ext behavior:
- sets
tenant1WorkflowNote - publishes event to
vehicle.events.testwith tenant header propagation
4. Packager (Both Tenants In One JVM)
Packager test runtime:
packager/service/src/test/java/com/mycompany/myorg/vehicle/packager/PackagerSpringTestConfig.java- imports
MultiTenantDataSourceConfiguration - scans
org.chenileandcom.mycompany
Packager config:
packager/service/src/test/resources/application.yml- defines tenant datasources under
chenile.multids.datasources - defines both subtype registrations under
chenile.http.extension-subtypes
Tenant context is header-driven:
x-chenile-tenant-id
4.1 Runtime Configuration (Packager Test Baseline)
The packager integration test runtime uses these key blocks:
chenile:
http:
extension-subtypes:
- name: tenant0_ext
className: com.mycompany.myorg.vehicle.model.VehicleExtensionTenant0
- name: tenant1_ext
className: com.mycompany.myorg.vehicle.model.VehicleExtensionTenant1
multids:
defaultTenantId: tenant1
datasources:
tenant1:
jdbcUrl: jdbc:h2:mem:packager-tenant1;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
tenant2:
jdbcUrl: jdbc:h2:mem:packager-tenant2;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
Why this matters:
extension-subtypesmaps JSONext_typeto concrete Java subtype.multidsroutes persistence per tenant in a single JVM runtime.defaultTenantIddefines fallback datasource when tenant is missing/invalid.
Reference:
5. TDD Flow (Read Tests First)
Tenant0 specs:
custom-tenant0/service/src/test/resources/features/service.feature
Tenant1 specs:
custom-tenant1/service/src/test/resources/features/service.feature
Combined packager specs:
packager/service/src/test/resources/features/multitenant-service.feature
Coverage includes:
- create + assign + ext + close
- tenant-specific extension column assertions
- pub/sub assertions with tenant propagation
- both tenants validated in same JVM runtime
5.1 Request/Execution Flow In This Sample
- Request carries
x-chenile-tenant-id. - Tenant header populates Chenile context.
- Multi datasource router resolves tenant datasource.
- Request body
ext_typeselects extension subtype (tenant0_ext/tenant1_ext). exttransition resolves tenant-specific action bean.- Action mutates tenant-specific fields and emits pub/sub event.
- Cucumber assertions verify both DB and event outcomes.
6. Pattern To Add New Tenant Extension
- Add tenant entity in new tenant
apimodule with newext_type. - Add tenant-specific transition action in tenant
servicemodule. - Add tenant bean configuration class.
- Add subtype mapping under
chenile.http.extension-subtypes. - Add cucumber scenarios first (TDD).
- Run tenant tests.
- Extend packager tests for multi-tenant same-JVM validation.
7. Commands
Run tenant0 tests:
mvn -pl custom-tenant0/service -am test
Run tenant1 tests:
mvn -pl custom-tenant1/service -am test
Run packager multi-tenant tests:
mvn -pl packager/service -am test
Run only packager cucumber suite:
mvn -pl packager/service -am -Dtest=com.mycompany.myorg.vehicle.packager.bdd.CukesRestTest -Dsurefire.failIfNoSpecifiedTests=false test
8. Technical Rules
- Core module owns mapper wiring (
VehicleConfiguration). - Tenant modules stay independent for extension logic.
- Packager is integration point for loading all tenant modules.
- Routing is header-driven (
x-chenile-tenant-id). - Tests are source of truth (TDD-first).
9. Current Limitation
When both tenant modules run in same JVM (packager), extension workflow definitions must currently be identical in structure.
Practical meaning:
- tenant-specific Java actions can differ
- workflow XML extension shape must remain same across tenants in this setup
This is a known current limitation and expected to improve in future.
Read More
- Chenile Samples Developer Guide
- Chenile Core Developer Guide
- Chenile Messaging Developer Guide
- Multi Datasource Utils Guide
Source
This guide follows:
how_to_extend_chenile_service_multitenant_pubsub/README.md- https://github.com/ajapros/chenile-samples/tree/main/how_to_extend_chenile_service_multitenant_pubsub