Process & Domain Specification

First we define our Business Processes and -Domain.

Process

Here we use the BPMN Specification with some extensions from Camunda. You can use Camunda's tools to create them, see Camunda Modeler.

To get started we will take the Invoice Example. You find a version for a version for Camunda 7 and one for Camunda 8 on Github - see Invoice Example

Camunda 7 BPMN

Nothing interesting here, just a standard Camunda/ BPMN Model. Let's define the Domain for this process.

Domain

Whenever we interact with the Process, der are Business Objects involved. But also a Service that we integrate defines its Domain.

So implementing a Process is more or less working with Domain Objects, mostly mapping them to similar Domain Object, defined by Services.

To describe the Domain Model, we use basic Scala constructs, like Case Classes and Enumerations.

The following BPMN Inputs / -Outputs do we describe:

Here is the UserTask 'Approve Invoice' from our Invoice Process.

You see here the following elements:

Checkout the Domain Description of InvoiceReceipt.

There are 2 things you have to care for that is not purely your domain.

Case Class

You describe your domain model with case classes, as they are easy to create and there is support for documentation and JSON marshalling.

Simple Enum

If an attribute of a Case Class is an enumeration you can use the Scala enum.

    enum InvoiceCategory :
      case `Travel Expenses`, Misc, `Software License Costs`

Hierarchy Enum

If you have inputs or outputs that can differ, you can use the Scala enum as well. It just looks a bit different:

  enum GetCodesOut:
    case KeyValues(
        codesResult: Option[Map[String, String]] = None,
        poBox: Option[Map[String, String]] = Some(defaultPoBoxFr)
    )

    case ManualKeys(
        codesResult: Option[Seq[Map[String, String]]] = None,
        poBox: Option[Seq[Map[String, Json]]] = Some(
          Seq(
            Map("key" -> 1.asJson, "name" -> "Postfach".asJson),
            ...
          )
        )
    )
  end GetCodesOut

Generic Objects

Sometimes your domain is huge are you will start fast. To have still at least an example and correct test data, you can use a generic datatype, like Json.

This looks like this:

case class MyDomainClass(
  ... // some specific attributes
  someJson: Json = toJson("""{"value": 12}""")
)

You can create examples with toJson("""{"value": 12}""").

This will be a Json Variable in the Simulations and you can use them everywhere in your domain model.

That this works you need this import: import sttp.tapir.json.circe.* - see JSON marshalling.

Tip: Start with Jsons and replace them with proper classes as soon you start documenting (mostly when the process is stable).

Documentation

The closer the documentation is to your code, that you work with, the higher is the chance that you will spot mistakes. So we use @description("my descr") from Tapir. These descriptions will then automatically taken into account when the API documentation is generated.

Be aware that the description of Input- and Output Objects are not taken into the doc by Open API.

So document them in the process description if needed.

JSON marshalling

We need this to get to and from JSON. There is an automatic way, but it turned out that it made compiling slow.

Sorry for this technical noise 😥.