Zurück zur WebseiteInsights

Mit bpmn-to-code deine Prozesse & Code in Einklang bringen

By Marco Schäck
Published in BPM
March 21, 2025
5 min read
Mit bpmn-to-code deine Prozesse & Code in Einklang bringen

Als Consultant & Entwickler bin ich stets davon angetrieben saubere und intelligente Lösungen zu schaffen - egal ob es sich um technische, architektonische oder organisatorische Herausforderungen handelt. Mein Ziel ist es, Hürden abzubauen & Lösungen zu entwickeln, die nicht nur mir, sondern auch anderen Personen ihre Arbeit erleichtern.

Ein Bereich, in dem mir immer wieder kleine, aber lästige Probleme begegnet sind, ist die Prozessautomatisierung mit BPMN und Engines wie Zeebe. Hier unterstütze ich unsere Kunden dabei, ihre Prozesse zu automatisieren und im Umgang mit BPMN sowie entsprechenden Prozess-Engines – sei es beim Modellieren oder bei der Implementierung – effektiv voranzukommen.

Ein häufiges Problem, das ich dabei sowohl bei mir selbst als auch bei unseren Kunden beobachte, ist die Notwendigkeit, technische Konfigurationen manuell aus BPMN-Modellen in den Code zu übertragen. Dazu zählen beispielsweise Element-IDs, Message-IDs und Worker-Types. Diese mühsame Aufgabe bringt zahlreiche Fehlerquellen mit sich und erhöht die kognitive Belastung – vor allem, wenn sich die Modelle weiterentwickeln und die Konfigurationen manuell aktuell gehalten werden müssen.

Um diese Probleme zu beheben, habe ich ein Maven- und Gradle-Plugin namens bpmn-to-code entwickelt. Es generiert etwas, das ich als Process‑APIs bezeichne. Code-Artefakte in Kotlin oder Java, die direkt aus BPMN-Modellen erstellt werden und sämtliche technischen Konfigurationen enthalten, die für die Interaktion mit einer Process-Engine notwendig sind.

Im Wesentlichen zielt das Plugin darauf ab, den manuellen Aufwand zu reduzieren, Fehler zu vermeiden und letztlich saubere & gut wartbare Lösungen zu entwickeln. Und wer weiß - vielleicht wird die Entwicklung mit BPMN und Process-Engines so für Entwickler auch ein Stück weit einfacher und intuitiver.

🕵️ Herausforderungen beim Einsatz einer Prozess‑Engine

Hand aufs Herz – die Arbeit mit Prozess-Engines verläuft selten so problemlos, wie man es sich wünscht. Stell dir vor, du wirst beauftragt, einen Newsletter-Abo-Prozess zu automatisieren. In aller Regel startest du damit, das Prozessmodell in BPMN zu entwerfen: Ein Nutzer füllt ein Formular aus, erhält eine Bestätigungs-E-Mail, bestätigt sein Abonnement und bekommt schließlich eine Willkommensmail. Sobald der grundlegende Ablauf steht, verfeinerst du das Modell, um Ausnahmen und zusätzliche Szenarien zu berücksichtigen.

In einer detaillierteren Form - oder (um ehrlich zu sein) in einer Form, welche die Verwendung verschiedener Elementtypen zeigt, die das Plugin verarbeiten kann - könnte das Prozessmodell dann in etwa so aussehen:

Beispiel für einen Newsletter-Abo-Prozess
Beispiel für einen Newsletter-Abo-Prozess

Sobald du den Prozess visuell modelliert hast, gehst du dazu über, ihn technisch zu konfigurieren, indem du z. B. Element- & Message-IDs hinterlegst. Danach kannst du das Modell in einer BPMN-Engine deiner Wahl, wie Camunda 7 oder 8, deployen. So weit, so gut. Aber dann kommt die eigentliche Implementierung.

Um den Prozess ausführbar zu machen, werden du zum Beispiel Service-Task-Worker implementieren und Nachrichten an die Engine über deren API senden. In diesem Stadium muss jeder Teil Ihres Codes auf die technischen Details des BPMN-Modells verweisen. Um diese Details abzurufen, müssen Sie sie normalerweise manuell aus dem Modell in Ihren Code kopieren. Eine nostalgische Praxis, die, seien wir ehrlich, im Jahr 2025 nicht mehr zeitgemäß sein sollte.

Das Ergebnis daraus sind etwa Service-Task-Worker, die auf hart-codierten Strings für basieren, wie im folgenden Beispiel illustriert:

@Component
class AbortRegistrationWorker(
    private val useCase: AbortSubscriptionUseCase
) : DefaultJobWorker(
    type = "newsletter.abortRegistration",
) {

    private val log = KotlinLogging.logger {}

    override fun executeTask(
        client: JobClient,
        job: ActivatedJob
    ): Map<String, Any?> {
        val input = job.getVariablesAsType(Input::class.java)
        log.debug { "Received job to abort registration: $input" }
        useCase.abort(SubscriptionId(input.subscriptionId))
        return emptyMap()
    }

    data class Input(val subscriptionId: UUID)
}

Und damit nicht genug: Wenn du außerdem auch noch Tests schreibst, kann es passieren, dass du mehrere Instanzen dieser hart-codierten Strings hast. Jede einzelne davon ist eine potenzielle Fehlerquelle - denn du musst sie einzeln in deinen Code kopieren und jederzeit auf dem aktuellen Stand halten.

@Test
fun `happy path - user subscribes to newsletter`() {

    val subscriptionId = UUID.fromString("4a607799-804b-43d1-8aa2-bdcc4dfd9b86")

    processPort.submitForm(SubscriptionId(subscriptionId))
    waitForProcessInstanceHasReachedElement("Activity_ConfirmRegistration")

    processPort.confirmSubscription(SubscriptionId(subscriptionId))
    waitForProcessInstanceHasPassedElement("EndEvent_RegistrationCompleted")

    assertThatProcess().isCompleted()
    assertThatProcess().hasPassedElementsInOrder(
        "StartEvent_RequestReceived",
        "Activity_SendConfirmationMail",
        "Activity_ConfirmRegistration",
        "Activity_SendWelcomeMail",
        "EndEvent_RegistrationCompleted"
    )

    verify { sendConfirmationMailUseCase.sendConfirmationMail(SubscriptionId(subscriptionId)) }
    verify { sendWelcomeMailUseCase.sendWelcomeMail(SubscriptionId(subscriptionId)) }
    verify { abortSubscriptionUseCase wasNot Called }
    confirmVerified(sendConfirmationMailUseCase, sendWelcomeMailUseCase, abortSubscriptionUseCase)
}

Zusammengefasst ist dieser Ansatz nicht nur frustrierend, sondern erzeugt auch ein regelrechtes Minenfeld. Ein einziger Tippfehler oder ein verpasstes Update kann dazu führen, dass dein Code nicht mehr mit dem Modell übereinstimmt. Wenn sich das Modell weiterentwickelt – was unvermeidlich ist – musst du jede Referenz manuell aufspüren und anpassen. Das Fehlerrisiko ist hoch, und der gesamte Prozess ist alles andere als effizient.

Stell dir nun zudem vor, du bist neu in der Welt von BPMN und Prozess-Engines. Begriffe wie Element-IDs oder Service-Task-Topics sind dir kaum geläufig. Kaum hast du begonnen, etwas zu entwickeln, stößt du auf diese Hürden und wirst schnell frustriert. Anstatt das Potenzial zu erkennen, lehnst du die Technologie ab und verpasst so den Mehrwert, den sie bieten könnte.

Nach einiger Erfahrung in der Entwicklung von Lösungen mit Process Engines und der Unterstützung von Kunden im Umgang mit BPMN kenne ich diese Herausforderungen nur zu gut. Genau deshalb habe ich bpmn-to-code entwickelt.

🚀 Eine Idee zur Lösung

bpmn-to-code ist ein Plugin, das sowohl für Gradle als auch für Maven verfügbar ist. Du kannst es dir vorstellen wie Swagger Codegen, jedoch speziell für BPMN und die Automatisierung von Prozessen mit entsprechenden Engines.

So wie Swagger Client-Code aus OpenAPI-Definitionen generiert, erstellt bpmn-to-code eine leichtgewichtige Process‑API aus deinen BPMN-Modellen. Diese API-Datei ist eine codebasierte Darstellung deines Prozessmodells – als Java- oder Kotlin-Datei – die alle relevanten technischen Konfigurationen wie Element-IDs, Message-IDs, Worker-Types und die Prozess-ID enthält.

Derzeit unterstützt das Plugin sowohl Camunda 7 als auch Zeebe und wurde mit Blick auf Erweiterbarkeit entwickelt. Das bedeutet, dass bei Bedarf auch weitere Engines unterstützt werden könnten.

Derzeit unterstützt das Plugin zwei Engines: Camunda 7 und Zeebe. Engines, mit denen ich lange gearbeitet habe, bzw. aktuell arbeite - weswegen ich einige Modelle hatte, um diese zu testen. Da es jedoch auch viele andere Engines gibt und Camunda 7 bald nicht mehr supported wird, habe ich das Plugin so entwickelt, dass es möglichst einfach um weitere Engines erweiterbar ist - sollte eine Nachfrage hierfür existieren.

🎮 Das Plugin in Aktion

Schauen wir uns nun an, was sich tatsächlich ändert, wenn du bpmn-to-code in deinem Projekt einsetzt. Zunächst einmal: Wie bringst du es zum Laufen? Nutzt du Gradle, füge einfach die folgende Plugin-Deklaration in deine build.gradle.kts ein. Nutzt du hingegen Maven findest du eine Anleitung in der README auf GitHub.

plugins {
    id("io.github.emaarco.bpmn-to-code-gradle") version "0.0.1-alpha"
}

Nun können du deine Dependencies neu laden. Sollte das Plugin noch nicht gefunden werden, stelle bitte sicher, dass du das Gradle Plugin Portal als Quelle für Plugins zu deiner settings.gradle.kts Datei hinzugefügt hast.

pluginManagement {
    repositories {
        gradlePluginPortal()
    }
}

Als Nächstes konfigurierst du den Generation-Task. In diesem Task legst du fest, wo sich deine BPMN-Modelle befinden, wohin der generierte Code geschrieben werden soll, in welcher Sprache die API erstellt werden soll, und mit welcher Prozess-Engine du arbeitest.

tasks.named("generateBpmnModelApi", GenerateBpmnModelsTask::class) {
    baseDir = projectDir.toString()
    filePattern = "src/main/resources/**/*.bpmn"
    outputFolderPath = "$projectDir/src/main/kotlin"
    packagePath = "de.emaarco.example"
    outputLanguage = OutputLanguage.KOTLIN
    processEngine = ProcessEngine.ZEEBE
}

Sobald du den Task ausführst, generiert das Plugin deine Process‑API. Zurück zu unserem Newsletter-Abonnement-Beispiel: Das Plugin erzeugt eine Kotlin- oder Java-Datei, die in etwa so aussieht:

package com.example.process

@Suppress("unused")
object NewsletterSubscriptionProcessApiV1 {
    val PROCESS_ID: String = "newsletterSubscription"

    object Elements {
        val Timer_EveryDay: String = "Timer_EveryDay"
        val Timer_After3Days: String = "Timer_After3Days"
        val ErrorEvent_InvalidMail: String = "ErrorEvent_InvalidMail"
        val Activity_ConfirmRegistration: String = "Activity_ConfirmRegistration"
        val SubProcess_Confirmation: String = "SubProcess_Confirmation"
        val EndEvent_RegistrationAborted: String = "EndEvent_RegistrationAborted"
        val EndEvent_SubscriptionConfirmed: String = "EndEvent_SubscriptionConfirmed"
        val EndEvent_RegistrationCompleted: String = "EndEvent_RegistrationCompleted"
        val EndEvent_RegistrationNotPossible: String = "EndEvent_RegistrationNotPossible"
        val Activity_AbortRegistration: String = "Activity_AbortRegistration"
        val Activity_SendWelcomeMail: String = "Activity_SendWelcomeMail"
        val Activity_SendConfirmationMail: String = "Activity_SendConfirmationMail"
        val StartEvent_SubmitRegistrationForm: String = "StartEvent_SubmitRegistrationForm"
        val StartEvent_RequestReceived: String = "StartEvent_RequestReceived"
    }

    object Messages {
        val Message_FormSubmitted: String = "Message_FormSubmitted"
        val Message_SubscriptionConfirmed: String = "Message_SubscriptionConfirmed"
    }

    object TaskTypes {
        val EndEvent_RegistrationCompleted: String = "newsletter.registrationCompleted"
        val Activity_AbortRegistration: String = "newsletter.abortRegistration"
        val Activity_SendWelcomeMail: String = "newsletter.sendWelcomeMail"
        val Activity_SendConfirmationMail: String = "newsletter.sendConfirmationMail"
    }

    object Timers {
        val Timer_EveryDay: BpmnTimer = BpmnTimer("Duration", "PT1M")
        val Timer_After3Days: BpmnTimer = BpmnTimer("Duration", "PT2M30S")

        data class BpmnTimer(
            val type: String,
            val timerValue: String,
        )
    }

    object Errors {
        val Error_InvalidMail: BpmnError = BpmnError("Error_InvalidMail", "500")

        data class BpmnError(
            val name: String,
            val code: String,
        )
    }

    object Signals {
        val Signal_RegistrationNotPossible: String = "Signal_RegistrationNotPossible"
    }
}

Dank dieser generierten Process‑API musst du dich nicht mehr auf hartkodierte Strings verlassen. Stattdessen referenzierst du die in der API bereitgestellten Variablen, wodurch dein Code direkt mit dem Modell verknüpft wird. Die technischen Details des Modells werden so zu einem integralen Bestandteil deines Codes – auf eine typensichere, lesbare und wartbare Weise. So verändert sich unser ursprüngliches Beispiel:

Unser Worker nutzt nun die Process‑API, um den Task-Type zu referenzieren:

import de.emaarco.example.NewsletterSubscriptionProcessApiV1
import ...

@Component
class AbortRegistrationWorker(
    private val useCase: AbortSubscriptionUseCase
) : DefaultJobWorker(
    type = NewsletterSubscriptionProcessApi.TaskTypes.Activity_AbortRegistration,
) {

    private val log = KotlinLogging.logger {}

    override fun executeTask(client: JobClient, job: ActivatedJob): Map<String, Any?> {
        val input = job.getVariablesAsType(Input::class.java)
        log.debug { "Received job to abort registration: $input" }
        useCase.abort(SubscriptionId(input.subscriptionId))
        return emptyMap()
    }

    data class Input(val subscriptionId: UUID)
}

Auch in deinen Tests werden alle string-basierten Referenzen durch Konstanten aus der Process‑API ersetzt.

import de.emaarco.example.NewsletterSubscriptionProcessApiV1
import ...

@Test
fun `happy path - user subscribes to newsletter`() {

    val subscriptionId = UUID.fromString("4a607799-804b-43d1-8aa2-bdcc4dfd9b86")
    processPort.submitForm(SubscriptionId(subscriptionId))

    waitForProcessInstanceHasReachedElement(Activity_ConfirmRegistration)
    processPort.confirmSubscription(SubscriptionId(subscriptionId))
    waitForProcessInstanceHasPassedElement(
            NewsletterSubscriptionProcessApi.Elements.EndEvent_RegistrationCompleted
    )
    
    assertThatProcess().isCompleted()
    assertThatProcess().hasPassedElementsInOrder(
        NewsletterSubscriptionProcessApi.Elements.StartEvent_RequestReceived,
        NewsletterSubscriptionProcessApi.Elements.Activity_SendConfirmationMail,
        NewsletterSubscriptionProcessApi.Elements.Activity_ConfirmRegistration,
        NewsletterSubscriptionProcessApi.Elements.Activity_SendWelcomeMail,
        NewsletterSubscriptionProcessApi.Elements.EndEvent_RegistrationCompleted
    )

    verify { sendConfirmationMailUseCase.sendConfirmationMail(SubscriptionId(subscriptionId)) }
    verify { sendWelcomeMailUseCase.sendWelcomeMail(SubscriptionId(subscriptionId)) }
    verify { abortSubscriptionUseCase wasNot Called }
    confirmVerified(sendConfirmationMailUseCase, sendWelcomeMailUseCase, abortSubscriptionUseCase)
}

Wichtig ist allerdings zu wissen: Das Plugin garantiert nicht, dass dein Code bei Modelländerungen synchron bleibt. Allerdings wird die Wartung deutlich vereinfacht, da du lediglich eine neue API-Version generieren und die betroffenen Bereiche anpassen musst.

🚀 Vorteile

Nachdem wir nun gesehen haben, was bpmn-to-code macht und wie es in einem Projekt verwendet werden kann, lass uns einen Schritt zurücktreten und die Hauptvorteile untersuchen, die das Plugin bietet:

  • Weniger manueller Aufwand: Das Plugin extrahiert alle notwendigen API-Elemente direkt aus dem BPMN-Modell und eliminiert mühsames Copy-Paste.
  • Schnellere Time-To-Market: Durch die automatische Generierung von Code-Artefakten optimierst du deinen Entwicklungsprozess und ermöglichst es, deinem Team, sich auf die Bereitstellung der Lösung zu konzentrieren.
  • Reduzierung von Fehlerquellen: Durch die automatische Generierung des Process-APIs wird die Wahrscheinlichkeit von Tippfehlern oder verpassten Aktualisierungen bei Modelländerungen erheblich reduziert.
  • Modell-Code-Konsistenz durch Versionierung: Da Process APIs jederzeit neu generiert werden können, bleibt Ihr Code mühelos mit dem Modell in Einklang. Jede API-Version erhält eine Versionsnummer, sodass Prozessänderungen nachvollziehbar und eindeutig sind.
  • Single-Source-of-Truth: Statt verstreuten Strings in deiner Codebasis, hast du eine zentrale Prozess-API Datei je Prozess. Die Datei fungiert als Repräsentation deines BPMN-Modells und dient als Referenzpunkt für deine Anwendung.
  • Verbesserte Transparenz: Wenn alle Prozessdetails im Code verfügbar sind, verbessern sich Lesbarkeit und Zugänglichkeit. Diese Transparenz hilft, Inkonsistenzen zu erkennen und fördert bessere Modellierungspraktiken, was letztendlich zu saubereren Lösungen führt.
  • Vertrauter Ansatz: Das Plugin orientiert sich an Tools wie Swagger, und zielt daher darauf ab, es Entwickler leichter zu machen in die Automatisierung von Prozessen mit Process Engines einzutauchen oder diese zu nutzen.

🏁 Fazit

Das manuelle Kopieren technischer Konfigurationen aus BPMN-Modellen in den Code ist nicht nur mühsam, sondern auch fehleranfällig. Es führt zu unnötigen Komplikationen, insbesondere wenn sich die Modelle weiterentwickeln. Genau dieses Kernproblem wollte ich mit bpmn-to-code lösen. Das Plugin generiert sog. Process‑APIs. Code-Artefakte, die deine BPMN-Modelle direkt in Kotlin oder Java abbilden. So bleiben Code und Modell synchron, der manuelle Aufwand sinkt. Alles in allem ermöglicht das Plugin saubere, robuste sowie entwicklerfreundliche Automatisierungslösungen.

Danke fürs Lesen!

Den kompletten Code und die Dokumentation zum Plugin findest du auf GitHub, falls du mehr darüber erfahren möchtest.

This post was originally published in English on Medium.com.


Tags

#bpm#bpmn#zeebe#bpmn-to-code#camunda7
Previous Article
Wie Wardley Mapping das Not-Invented-Here Syndrom vermeiden kann
Marco Schäck

Marco Schäck

Fullstack Entwickler

Inhalte

1
🕵️ Herausforderungen beim Einsatz einer Prozess‑Engine
2
🚀 Eine Idee zur Lösung
3
🎮 Das Plugin in Aktion
4
🚀 Vorteile
5
🏁 Fazit

Ähnliche Beiträge

Warum übergreifende BPMN-Prozesse gefährlich für moderne Organisationen sind
March 27, 2025
3 min
© 2025, All Rights Reserved.

Links

StartseiteLösungenBPM-TrainingKarriereInsights

Social Media