Skip to main content

Architecture

The Process Context Service (PCS) is a generic, largely passive service that provides a process context to the microservices participating in a process in an event driven architecture.

Goals

In an orchestration based architecture, the process context is maintained by a process engine. It essentially comprises the process request, the state of the individual process steps and their results. Based on that information the orchestrator decides which activities are required, when an activity can be started, and parameterises the activities it starts.

An event driven architecture has no central orchestrator: services react to events on their own and execute the required actions. That has advantages, but also drawbacks:

  • Transparency — which process instances exist, what state are they in, what has happened so far?
  • Data flow is harder, especially with indirect dependencies between worker services.
  • Complex dependencies — as long as an activity is started by exactly one event this is simple, but as soon as several events have to be combined (join) it quickly becomes complex.

The PCS addresses these drawbacks by providing a central process context to the autonomously acting services.

The PCS is strictly passive

It does:

  • instantiate process instances when it receives a message that is configured to do so,
  • listen to messages (events, commands) of the workers and update the process context,
  • publish events about changes of the process context that workers can react to.

It is not an orchestrator and must never:

  • decide which activities are to be executed — that is the responsibility of the workers,
  • trigger an activity by sending a command.

Domain terms

TermMeaning
ProcessA set of logically connected activities (tasks) executed to reach a certain goal.
Process templateDefines how a process runs: the expected tasks and their cardinality.
Process instanceAn instance of a process, created based on a process template.
Task typeDescription of a task within a process, in particular by task name and cardinality.
Task instanceAn instance of a task. Depending on the cardinality there can be several instances per task type.
Mandatory single-instance taskTask with a cardinality of 1, has to be executed exactly once in the process.
Optional single-instance taskTask with a cardinality of 0..1.
Multi-instance taskTask with a cardinality of 0..n.
Pending messageA message for which no process instance existed yet when it was received. It is assigned to the process instance as soon as that instance is created.
Process snapshotThe state of a process instance at a given point in time, for archiving by a Process Archive Service instance.

Quality goals

PrioCategoryQuality goalRationale
1Functional suitabilityCorrectnessTask and process states must be maintained consistently and the resulting events produced correctly — including under the asynchronous nature of event communication and eventual consistency.
2PortabilityAdaptabilityTeams must be able to fulfil their process tracking requirements through configuration.
3MaintainabilityReusabilitySeveral teams use the same basis for the process context service of their business application.
4UsabilityComprehensibilityConfiguring process templates must be understandable and simple.
5SecurityAuthenticityAccess is protected by authentication and authorization (roles on UI and APIs, permissions on topics).
6ReliabilityFault tolerancePlugins, asynchronous communication with eventual consistency and temporary inconsistencies between instances and templates during deployments are possible sources of error; the service is designed to cope with smaller inconsistencies and to handle the rest robustly.

High availability is explicitly less important: because of the decoupling and the passive character of the service, and because the frontend serves traceability and analysis rather than business-critical operations.

Context

Business context

Data flowDescription
Messages (domain events, commands)Messages relevant for the process state. They contain a process ID or can be correlated to a process instance via a reference or their payload. They can create new process instances if the template says so.
ProcessSnapshotCreatedEventEvent notification when a process snapshot has been created.
Process stateThe state of the process and its tasks, presented as a checklist in a user interface, and provided as a process snapshot for archiving.

Technical context

ComponentChannelDescription
BrowserHTTPSRead-only access to the REST API to display the state of a process.
MicroservicesKafkaProduce domain events that the PCS consumes to track the state of the process instances.
Process Archive ServiceKafka, HTTPSIs informed about new snapshots by the ProcessSnapshotCreatedEvent and fetches the snapshots for archiving via the REST API.
Error Handling ServiceKafkaMessageProcessingFailedEvents for messages whose processing failed are produced to the error topic; retries are published back to the original topic.

Building blocks

ComponentDescription
Domain servicesDomain logic and access across several aggregate roots, transaction control.
Domain modelDomain logic within one aggregate root (e.g. state changes while processing messages).
Message consumer portReceives messages that can lead to changes of the process state.
Query portProvides query capabilities for processes (implemented as a read-only repository).
Event producer portProduces the process status notification events.
ProcessInstance repository (port)Repository interface for process instances.
ProcessSnapshot repository (port)Repository interface for persisting process snapshots.

REST API

PathVerbResponseAuthorization
/api/processes/GETProcess instances (paged)processinstance / view
/api/processes/{originProcessId}GETProcess instanceprocessinstance / view
/api/processes/{originProcessId}/messagesGETMessages (paged)processinstance / view
/api/processes/{originProcessId}/process-dataGETProcess data (paged)processinstance / view
/api/processes/{originProcessId}/process-relationsGETProcess relations (paged)processinstance / view
/api/processes/{originProcessId}/relationsGETRelations (paged)processinstance / view
/api/snapshot/{originProcessId}GETA process snapshot versionprocesssnapshot / view

The process view role is typically granted to the business user of the frontend via the OAuth authorization code flow. An up-to-date description of the API is served by the running application at /swagger-ui/index.html.

In addition, /api/configuration, /api/configuration/version and /api/configuration/log-deeplink serve the bundled Angular UI with its OIDC configuration, the application version and the log deep link template.

Aggregate roots

AggregateDescription
Process instanceProcess instance with its state, task instances and process template.
Process templateThe template defining the tasks to be executed in a process.
Pending messageMessages that could not yet be correlated to an existing process instance when they were received.
MessageMessages (domain events, commands) with their message key/value data.
Process snapshotThe representation of a process instance at a given point in time.

A process snapshot deliberately does not represent the complete state of a process instance, only the part needed for traceability. Snapshots are stored in the Avro format recommended for archiving by the Process Archive Service.

Messages

Published events

Event typeReferencesPayloadDescription
ProcessSnapshotCreatedEventoriginProcessIdsnapshotVersionA new process snapshot version was created.

Consumed messages

Domain events and commands, depending on the business application. References to process and task are extracted by correlation providers, data by custom payload and reference extractors.

Internal messages

Internal Kafka messages decouple the processing steps inside the PCS. They decouple the processing from the partitioning of the source topics, shorten transactions, simplify error handling and generally increase robustness. These technical messages are modelled as jEAP domain events.

Topic (configuration key)Message typePayloadDescription
jeap.processcontext.kafka.topic.process-outdated-internalProcessContextOutdatedEventoriginProcessId, processUpdateType (MESSAGE_RECEIVED, PROCESS_CREATION_MESSAGE_RECEIVED, MIGRATION_TRIGGERED), messageId, templateNameSomething happened for a process instance that potentially affects its state → the state has to be recomputed (or the instance created).

Cross-cutting concepts

Architectural pattern

Ports and adapters, with an active domain model implementing the domain logic:

  • the domain model is at the centre and can be tested separately,
  • it is not anaemic — no models consisting of pure getters and setters,
  • computation of state transitions is encapsulated centrally in the model instead of being distributed,
  • plugins of the business applications are implemented as adapters against ports.

Persistence

  • Process instance: the domain model is persisted directly with JPA annotations on the fields of the class. This avoids unnecessary mapping between JPA and the domain model; an independent persistence model would be desirable but hard to justify given the mapping effort.
  • Process template: loaded as JSON, persisted in the database.
  • Process snapshot: stored in an S3 object store as an Avro binary, together with the metadata required by the Process Archive Service.

Asynchronous notifications and eventual consistency

Message processing per process instance must not happen in parallel. Serial processing per process instance is essential — the order does not matter, only the avoidance of race conditions. This is achieved with the internal processing events described above: they use the process instance ID as the Kafka record key, which serialises the consumption of the events per process instance ID.

Late correlation

Correlation based on process data allows a message to be assigned to a process instance based on information that becomes known to the instance only after the process has started.

  • If the message writing the process data is processed before the message to be correlated, the correlation already happens in the ProcessUpdateService by searching for a process with the relevant process data (early correlation).
  • If the messages arrive in the opposite order, that search finds nothing. The message is stored first and correlated later, when the relevant process data becomes known (late correlation). Late correlation happens in the ProcessInstanceService, which writes the process data: whenever it creates new process data, it checks whether already received messages can now be correlated.

Correlation must always happen, even if an early and a late correlation attempt occur practically at the same time. To prevent race conditions, processing in both services is split into two transactions: first the data derived from the message is stored, then the database queries detecting correlations are executed. This guarantees a correlation regardless of how the transactions of the two services interleave.

Idempotency

  • Process creation is idempotent via the origin process ID: if an instance already exists, the creation is ignored.
  • Events triggered by state changes are published successfully before the triggering message is acknowledged. If processing fails, the original message is passed to the error handling and acknowledged; a retry then relies on the idempotent processing again.

Error handling

The Error Handling Service is used for errors occurring while processing a message. Errors are classified as:

ExceptionClassification
org.springframework.dao.TransientDataAccessExceptionTemporary
org.apache.kafka.common.errors.RetryableExceptionTemporary
Everything elsePermanent

Logging

LevelUsageExamples
ERRORErrors
WARNUnexpected states that are not necessarily errors
INFOState transitions in processesStartup information, process instance creation, process state changes, planning and completion of tasks
DEBUGFurther details for analysisRequest/response details, consumed and produced records

Design decisions

Architectural pattern

Decision: ports and adapters with an active domain model.

Rationale: clear structure, transaction boundaries and a clear location for the domain logic; separation of infrastructure and domain; plugins of the business applications can be implemented against ports. The alternative considered was a layered architecture, which gives a less clearly defined structure.

Persistence

Decision: the domain model is persisted directly with JPA.

Rationale: lower effort and complexity. A move to event sourcing remains possible if it later turns out to be necessary.

OptionDescriptionAdvantagesDisadvantages
1 (chosen)Domain model persisted directly with JPALow effort, simple implementationDomain model has JPA dependencies; only the current state is known, not the history
2Domain model in memory only, persistence via event sourcingGood traceability, good expected performance through inserts onlyDatabase harder to analyse, higher implementation effort, more complex through the decoupling of model and persistence

Asynchronous processing of messages affecting the process state

Decision: an internally event driven architecture. The algorithm has three stages:

  1. consume the incoming message, persist the message type in the process context and produce a technical event to update the process and task state,
  2. compute the updates of the process and task state and produce a technical event to trigger the reactions to the new state,
  3. compute the reactions (snapshots, completion), persist them and produce the corresponding domain events.

The integration between the three stages happens over technical Kafka events. This keeps the transaction boundaries small, and the partitioning — and with it the parallelism — independent of the source topics.

Rationale: clean decoupling, no database-centric design that would not scale.

Alternatives considered were pessimistic locking on a dedicated lock table (scales badly, optimises for the special case of parallel processing), forwarding the incoming events to an internal topic keyed per process instance (requires a message contract for every forwarding, exposing teams to PCS internals), and optimistic locking (acceptable, but may produce state events more than once).