Skip to content

feat(domain,json)!: BPMN-aligned sealed domain model and process JSON v2 - #81

Merged
emaarco merged 1 commit into
mainfrom
issue-58
Aug 13, 2026
Merged

feat(domain,json)!: BPMN-aligned sealed domain model and process JSON v2#81
emaarco merged 1 commit into
mainfrom
issue-58

Conversation

@emaarco

@emaarco emaarco commented Aug 11, 2026

Copy link
Copy Markdown
Member

What

Redesigns the process JSON and the domain model against BPMN 2.0 / bpmn-moddle, and unblocks #73 and #74.

The old properties slot held exactly one facet per node, so a multi-instance service task with an I/O mapping was literally unrepresentable — which is why both features land here rather than before.

  • ADR 017 — sealed, BPMN-aligned FlowNodeDefinition. Sub-processes own their children and their own sequence flows; typed facets (TaskImplementation, EventDefinitionInstance[], MultiInstanceDefinition, IoMapping, EngineExtension) replace the untyped maps.
  • ADR 018 — schema-versioned JSON v2: real containment, sequence-flow relations, multi-trigger events, root-element registries keyed by their own id.

Impact

  • Breaking: the process JSON format. Migration guide: docs/changelog/v6.md.
  • Breaking: custom validation rules that cast context.model to BpmnModel / MergedBpmnModel — those types are gone (see below).
  • Generated Kotlin/Java Process API is unchanged except for models declaring a root element nothing references — those now emit one extra constant and are reported by a new rule. Measured against the merge-base over every fixture × 3 engines × 2 languages: 4 of 16 files, one constant each.

Also in here

Five defects found while validating the redesign, each with a failing test first:

  • Dangling references after a merge — extraction keyed registries by id, merging by name, so two same-named bpmn:Message elements collapsed into one and left a messageRef pointing at nothing.
  • Under-reported validationserviceTasks deduplicated by implementation reference (empty when unconfigured), so N unimplemented tasks produced one violation.
  • isExecutable on merged models was hardcoded true, and the JSON paths don't filter.
  • Correlation key on the wrong entityzeebe:subscription belongs to the bpmn:Message, not to each referencing event.
  • Compensation activityRef repeated the event's own id.

Structure

adapter/outbound/engine was split by class kind (reader/, extractor/, utils/) with two of them importing each other; it is now split by problem — bpmn/, dialect/, xml/ — and EngineModelExtractor takes a dialect instead of being subclassed per engine.

BpmnModel and MergedBpmnModel differed only in whether variants were present, and every consumer asked exactly that. They collapse into one ProcessModel with isMerged. The four root-element registries move behind a RootElements value object, and BpmnModelApi.engine becomes targetEngine — distinct from ProcessModel.detectedEngine, which is what EngineMismatchRule compares it against.

Deliberately not done

VariableMapping is a code-generation ABI expressed as a domain interface, but moving it forces a decision on whether "will the generated API collide?" is a domain or an adapter rule. Likewise, root-element names are still copied onto the node tree because the correlation rules match on names rather than …Ref. Both are recorded in ADR 017 — they deserve their own ADR, not a release.

Verification

./gradlew clean build green · 387 tests, 0 failures · coverage gate green · architecture tests green under --rerun-tasks.

Generated JSON is validated against the published schema on every run, for every fixture and engine, plus a reference-integrity check. Coverage for #73/#74 spans all three engines at both the extraction and JSON layers; both guards were verified to fail under mutation.

Closes #58
Closes #73
Closes #74

@emaarco
emaarco marked this pull request as ready for review August 11, 2026 16:04
@emaarco
emaarco force-pushed the issue-58 branch 3 times, most recently from 02d7d3b to 1169a2c Compare August 13, 2026 10:16
Redesigns the generated JSON against the OMG BPMN 2.0 metamodel and the
bpmn-moddle vocabulary, and reshapes the domain model to match. Two open
feature requests forced the timing: neither multi-instance loop characteristics
nor zeebe:ioMapping could be expressed by the old single-slot properties field,
so #58 had to land first and they ship inside it.

Domain

FlowNodeDefinition is a sealed hierarchy mirroring the BPMN class tree, so a
multi-instance gateway or a calledElement on an event is unrepresentable rather
than merely unused. ProcessModel replaces BpmnModel and MergedBpmnModel; a
process is a bpmn:FlowElementsContainer like any sub-process, and everything
derivable — timers, compensations, implementations, variables — is derived from
the node tree instead of stored beside it.

JSON

Relations are sequence-flow ids, sub-processes own their children and their own
flows, events carry every event definition rather than the first, and the root
element registries are keyed by the element's own id so a message used three
times appears once. The document carries $schema and formatVersion, validated
against a published JSON Schema.

The raw layer carries only what the typed layer does not: an extension element
or attribute a dialect reads in full is no longer repeated beside its normalised
form.

Boundaries

Adding an engine means adding an EngineDialect; the per-engine extractors are
gone. bpmn-to-code-testing goes through an inbound port instead of reaching into
the engine adapter, which lets all 42 types under adapter/outbound become
internal.

Closes #58
Closes #73
Closes #74

BREAKING CHANGE: The process JSON is a new contract — see docs/changelog/v6.md
for the field-by-field migration. The domain model's public types changed:
BpmnModel and MergedBpmnModel are replaced by ProcessModel, FlowNodeProperties
and BpmnNodeType are absorbed into the sealed FlowNodeDefinition hierarchy, and
everything under adapter.outbound is now internal. The generated Kotlin and Java
Process API is unchanged apart from unreferenced bpmn:Definitions root elements,
which are now reported by a validation rule instead of silently dropped.
@github-actions

Copy link
Copy Markdown
Contributor

Test Coverage

Module Line Coverage
bpmn-to-code-core 97.6% (2155/2209 lines)
bpmn-to-code-gradle 25.7% (29/113 lines)
bpmn-to-code-maven 62.2% (28/45 lines)
bpmn-to-code-web 95.6% (196/205 lines)
bpmn-to-code-testing 93.7% (164/175 lines)
bpmn-to-code-runtime 98.0% (48/49 lines)

@emaarco
emaarco merged commit a1d21b9 into main Aug 13, 2026
5 checks passed
@emaarco
emaarco deleted the issue-58 branch August 13, 2026 12:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant