Skip to main content
Version: 2.0-rc2

Orchestrated Saga Pattern

Problem​

Some provisioning workflows in CIVITAS/CORE require sequential, cross-adapter operations where each step depends on outputs from previous steps. If a later step fails, all previously completed steps must be rolled back to maintain consistency.

Example: DatasetDatasetA data-related element that contains processed data and makes it available for consumption. A Dataset is populated via Pipelines and carries Metadata and access permissions. / Dataspace Provisioning

StepSystemActionOutputDepends On
1FROST ServerCreate projectprojectId, internal URL--
2APISIXApache APISIXAn open-source API gateway for traffic management, security and observability. In CIVITAS/CORE it is used as the centralized entrypoint to route and protect externally exposed APIs. GatewayCreate read-only routeExternal URLStep 1: internal URL
3Apache NiFiApache NiFiA stream processing and connector framework. In CIVITAS/CORE it is the pipeline engine for data integration and transformation (see ADR 047) and implements dataset-defined data flows.Deploy pipelinesPipeline IDStep 1 + Step 2

If step 3 fails after steps 1 and 2 succeeded, the APISIXApache APISIXAn open-source API gateway for traffic management, security and observability. In CIVITAS/CORE it is used as the centralized entrypoint to route and protect externally exposed APIs. route and FROST project must be removed.

The existing Config Adapter framework handles independent, stateless operations well, but cannot manage these cross-adapter dependencies.

Why Orchestration over Choreography​

A choreography-based approach was evaluated and rejected (ADR 031). With three or more sequential steps, choreography introduces:

  • Implicit workflow coupling -- each adapter must know its predecessor and successor
  • Cascading compensation -- rollback propagates backwards; if any compensation fails, subsequent compensations never trigger
  • Distributed routing logic -- adapters accumulate responsibilities beyond their domain
  • Bloated event payloads -- the initial event must carry configuration for all adapters

Solution: Dedicated Orchestrator​

A dedicated config-adapter-orchestrator module centrally manages multi-step provisioning workflows. Adapters remain simple command handlers with no workflow knowledge.

Architecture Overview​

Responsibility Split​

ComponentResponsibilities
OrchestratorWorkflow execution, saga state management, data mapping between steps, compensation on failure, status reporting
AdaptersPure command handlers -- receive a command, execute it, return a result. No workflow knowledge, no awareness of sagas or compensations

An adapter does not know whether a delete command is a regular operation or a compensation rollback. It executes the same logic in both cases.

Success Flow​

Failure Flow with Compensation​

When a step fails, the orchestrator triggers parallel compensation for all previously successful steps:

Saga State Machine​

StateDescription
PENDINGSaga created, not yet started
EXECUTINGSteps are being executed sequentially
COMPLETEDAll steps finished successfully
COMPENSATINGA step failed; compensating previous steps
COMPENSATEDAll compensations completed successfully
COMPENSATION_FAILEDA compensation failed; requires manual intervention

Workflow Definition​

Workflows are defined declaratively. Adding a new backend (e.g., Stellio/NGSI-LDNGSI-LDAn Open API and data model specification for context management, published by ETSI. It defines how context information (entities, relationships, and properties) is represented and exchanged.) means adding a new workflow definition -- no existing adapter code changes.

workflows:
dataspace-create-frost:
trigger: "dataspace.create.requested"
condition: "payload.backendType == 'FROST'"
steps:
- name: create-frost-project
command: "frost.project.create"
input:
projectName: "{{payload.datasetName}}"
output:
projectId: "{{result.resultData.projectId}}"
baseUrl: "{{result.resultData.baseUrl}}"
compensate:
command: "frost.project.delete"
input:
projectId: "{{steps.create-frost-project.output.projectId}}"

- name: create-apisix-route
command: "apisix.route.create"
input:
uri: "/api/dataspace/{{payload.datasetId}}/*"
upstreamUrl: "{{steps.create-frost-project.output.baseUrl}}"
methods: ["GET"]
output:
routeId: "{{result.resultData.routeId}}"
compensate:
command: "apisix.route.delete"
input:
routeId: "{{steps.create-apisix-route.output.routeId}}"

- name: deploy-pipelines
command: "redpanda.pipeline.deploy"
input:
pipelineJson: "{{payload.pipelineJson}}"
targetUrl: "{{steps.create-frost-project.output.baseUrl}}"
compensate:
command: "redpanda.pipeline.delete"
input:
pipelineId: "{{steps.deploy-pipelines.output.pipelineId}}"

Saga State Persistence​

The orchestrator requires persistent storage for saga state. PostgreSQL is recommended, as the platform stack already includes it.

OptionProsCons
PostgreSQL (shared)Proven, transactional, queryableShared DB dependency
PostgreSQL (dedicated)Full isolationAdditional infrastructure
KafkaApache KafkaA distributed event streaming platform. In CIVITAS/CORE it is used as the message bus to transport events, models and data in data flows. Streams state storeNo external DBHarder to query, operational complexity

API Extension​

The ConfigResultEvent includes an additional field to carry structured result data from adapters:

// Backwards-compatible, nullable
private Map<String, String> resultData;

Existing adapters that do not set resultData continue to work via the existing resourceId field.

Trade-offs​

BenefitTrade-off
Adapters stay simple (no workflow logic)Additional deployment unit (orchestrator)
Central workflow visibility and queryable stateRequires a database for saga state
Parallel compensation (not cascading)Orchestrator must be highly available
Scalable to new backends via workflow definitionsOrchestrator framework must be built
Existing adapters remain unchangedSingle point of coordination