Skip to content

About

A ready-to-fork bike-leasing blueprint on Operaton with an embedded engine, wired through the bpm-crafters process-engine-api β€” one complete, production-shaped BPMN service in Spring Boot & Kotlin 🚲

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

26 Commits

Folders and files

Repository files navigation

Operaton Bike-Leasing Blueprint

Note

🚧 Work in progress. This is a solution template β€” a reference to fork and build on, for our consultants and anyone else β€” not a product that ships. It's still being fleshed out, so parts may be incomplete and it may not yet fully demonstrate what it's meant to. Treat it as a living example, and expect it to keep evolving.

A ready-to-fork starting point for automating a business process on Operaton (the community fork of Camunda 7) with an embedded engine, Spring Boot and Java β€” one complete, runnable, production-shaped BPMN service you can clone and make your own.

This variant talks to the engine through the process-engine-api (bpm-crafters) abstraction instead of the plain Operaton APIs: the BPMN service tasks are external tasks consumed by @ProcessEngineWorker beans, and starting instances / correlating messages / completing user tasks all go through the process-engine-api. The engine is still embedded, so Cockpit/Tasklist and the /engine-rest API remain available.

The scenario

Meet MiraVelo β€” a (fictional) lifestyle bike brand for the quarter-life-crisis crowd: gravel bikes for the weekends that count, road bikes for everyone who just wants to feel the asphalt. MiraVelo sells its bikes on a leasing model for private and corporate customers, and this project automates that leasing application from the first request to an active lease.

It's a made-up company, so nobody gets hurt when the DMN politely declines a 15-year-old's application for a carbon road bike.

What's inside

Most engine examples stop at a happy-path service task. This one deliberately walks through the broad palette of BPMN elements you actually meet in real processes β€” and the engineering scaffolding around them β€” so a new project starts from something complete instead of a blank page:

The bike-leasing process

  • a message start event, service tasks (process-engine-api workers) and a DMN business-rule task;
  • an embedded sub-process with an event-based gateway (sign vs. a 14-day deadline) and a non-interrupting 7-day reminder timer;
  • a parallel fork/join, and a user task with a Camunda Form β€” completable in the Tasklist or via a REST endpoint;
  • compensation / SAGA handlers guarded by error and escalation boundary events;
  • a call activity into a second process, a message event sub-process (application withdrawal), and a terminate end event.

How it's built

service/
  common-architecture-tests/   reusable ArchUnit rule suite (src/main)
  app/                         the Operaton bike-leasing service (hexagonal)
    adapter/inbound/rest        domain REST controllers
    adapter/inbound/operaton    process-engine-api workers for the BPMN service tasks
    adapter/outbound/operaton   drives the engine through the process-engine-api
    adapter/outbound/db         JPA persistence (leasing applications + bike portfolio)
    adapter/outbound/dealer     simulated bike dealer (stock check + order)
    adapter/process             generated *ProcessApi (bpmn-to-code) + engine config
    application/{port,service}  use-case ports and their services
    domain/{leasing,bike}       pure domain model
    resources/{bpmn,dmn,forms}  the process models and Camunda Forms
    resources/db/migration      Flyway forward-only migrations
openapi/openapi.json           generated by a test, committed, drift-gated in CI
bruno/                         REST scenarios (happy-path / escalation / abort / not-solvent / …)
package.json                   BPMN linting (bpmnlint) β€” tooling at the repo root
checkstyle.xml                 source-style rules (one top-level class per file, no star imports)
stack/                         Postgres dev stack (docker compose)
docs/{README.md, adr/, assets/} ADR index + records + the process diagram
.github/                       pre-merge pipeline + Dependabot
  • Stack: Java 21 Β· Spring Boot 4 Β· Operaton 2.1 (embedded) Β· process-engine-api (bpm-crafters) Β· PostgreSQL Β· Maven multi-module (./mvnw), versions centralized in the root pom.xml.
  • Generated process API: the bpmn-to-code Maven plugin turns each .bpmn into a typed *ProcessApi class, so element ids, messages, timers and variables are compile-checked constants used by both delegates and tests.
  • Forms: Camunda Forms (.form) are deployed with the process and render in the Operaton Tasklist/Cockpit for the user tasks.
  • BPMN linting: bpmnlint (bpmnlint:recommended plus the camunda-platform-7 and @miragon/rules plugins) gates the .bpmn models. The tooling lives at the repo root β€” npm ci && npm run lint:bpmn β€” and runs in CI before the Maven build.
  • API contract: springdoc generates openapi/openapi.json from the controllers; it is committed and drift-gated in CI, so any REST change that isn't regenerated fails the build.
  • Database: schema is owned by Flyway forward-only migrations (resources/db/migration); Hibernate only validates.
  • Observability: Spring Boot Actuator exposes health (with liveness/readiness probes) and Prometheus metrics under /actuator.

Design decisions

  • Hexagonal architecture keeps the engine and framework at the edges: the domain and use cases never depend on Operaton, so business logic is testable and the engine is replaceable. The service/common-architecture-tests module enforces this with ArchUnit (bytecode: layering, dependency direction, naming) β€” one line wires it into a service: class ArchitectureTest extends ServiceArchitectureTest. Source-level rules (one top-level class per file, no wildcard imports) are enforced by Checkstyle (checkstyle.xml at the repo root) inside ./mvnw verify.
  • Unit tests (JUnit 5 + Mockito in BDD style) cover every domain type, application service and adapter with given/when/then comments and shared testLeasingApplication(...) builders β€” controllers via @WebMvcTest, persistence via @DataJpaTest. The workers are covered by the process tests.
  • Mutation testing (pitest) gates PRs at a score of 80: coverage says a line ran, mutation says a test would have noticed. It runs diff-scoped on PRs and a full-module sweep nightly.
  • Process tests (operaton-bpm-assert) drive the deployed model β€” timers and async continuations are fired and messages correlated by hand, while the real @ProcessEngineWorker beans consume the external service tasks β€” covering happy-path, escalation, abort, DMN rejection, the bike-unavailable β†’ alternative-selection loop, and a declined alternative.
  • Model validation (bpmn-to-code-testing) checks the .bpmn models structurally at build time (BpmnRules.all() plus a custom rule requiring every service task to be an external task with a topic).
  • Bruno + CI proves the same scenarios against the running app: domain REST endpoints drive the business actions, and the Operaton /engine-rest API completes user tasks and fires timer jobs so the whole flow runs in the pipeline without real 14-day waits.
  • Dependabot keeps the Maven dependencies, the Postgres image and GitHub Actions current (Maven wrapper bumps are manual).

Every non-obvious decision is recorded as an Architecture Decision Record β€” start at the docs index and its ADRs (0001–0014) to read the why before changing the what.

Run it

# 1. start Postgres
docker compose -f stack/docker-compose.yml up -d

# 2. run the app (Operaton Cockpit/Tasklist at http://localhost:8080/operaton, admin/admin)
#    fresh clone? run `./mvnw -DskipTests install` once first
./mvnw -pl service/app spring-boot:run

# 3. lint the BPMN models (tooling lives at the repo root)
npm ci && npm run lint:bpmn

# 4. drive the scenarios (build + arch + process tests first, then the REST flows)
./mvnw verify
cd bruno && npx --yes @usebruno/cli@4.0.0 run . --env local -r

Prefer containers? ./mvnw -DskipTests install && ./mvnw -pl service/app spring-boot:build-image -DskipTests builds an OCI image (operaton-process-engine-api-example/app:…) with Spring's buildpacks β€” no Dockerfile β€” to run against the Postgres dev stack. See CONTRIBUTING.md for the details.

Start a case with POST http://localhost:8080/api/bike-leasing ({ "customerName": …, "email": …, "age": 35, "monthlyNetIncome": 3500, "bikeId": "BIKE-900", "bikeModel": "Gravel Explorer 900" }).

The age and monthlyNetIncome feed the checkCreditRating DMN; the bikeId identifies the bike and is the only bike attribute the engine ever carries. The descriptive bikeModel lives in a separate bike portfolio aggregate (its own bike_portfolio table, keyed by bikeId) β€” never as a process variable β€” and GET /api/bike-leasing/{id} resolves it back from there. Alongside it a small set of read endpoints round out the API: GET /api/bike-leasing lists applications (paged), GET /api/bikes returns the bike portfolio, and GET /api/tasks/clarify-alternative lists the open clarification tasks. Every endpoint is described by the committed, drift-gated openapi/openapi.json.

If the requested bike is out of stock, the Clarify alternative with customer user task shows a deliberate contrast:

  • it reaches the domain either way β€” a client calls POST …/api/bike-leasing/{id}/clarify-alternative, or a human completes the Camunda Form in the Tasklist. Both hand the chosen bikeId to the process, and the re-order reads it and persists it on the application. The REST call additionally registers the bike's model in the portfolio; versus
  • the form-only path on clarify-return in cancel-bike-order.bpmn, kept as a counter-example: completing it via the Camunda Form or /engine-rest never touches the domain, so its data lands only in process variables (see the bpmn:documentation on each task).

Bike availability itself is decided by a BikeDealerPort outbound adapter (checkAvailability / order) whose small out-of-stock deny-list drives the branch: an out-of-stock bike leaves the order task through the bikeUnavailable error boundary event, so no order compensation is registered.

Incident demo

Want to teach transaction boundaries, retries and incidents? Submit a request for the poison bike BIKE-FAIL: the simulated dealer "outage" fails the Order bike from dealer external task, its retries count down to zero (3 attempts, per the adapter's retry policy), and an incident appears in the Cockpit to analyze and retry. A ready-to-run Bruno collection lives in bruno/06-incident-demo/.

Contributing

Contributions are welcome. Please open an issue to discuss substantial changes first, keep the architecture tests green (./mvnw verify), and use Conventional Commits for commit messages and PR titles.

License

Licensed under the MIT License.

About

A ready-to-fork bike-leasing blueprint on Operaton with an embedded engine, wired through the bpm-crafters process-engine-api β€” one complete, production-shaped BPMN service in Spring Boot & Kotlin 🚲

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages