Skip to main content
Version: 2.0.0

ADR 031: Orchestrated Saga for Multi-Adapter Provisioning

Date: 2026-02-11 Status: Accepted

Decision Makers: Architecture Board

Context​

The Config-Adapter Framework currently handles independent, stateless operations: each adapter receives a command event, configures its target system, and publishes a result event. This works well for isolated tasks like creating a KeycloakKeycloakAn open-source Identity and Access Management (IAM) solution providing SSO and OAuth2/OpenID Connect flows. In CIVITAS/CORE it is used to authenticate users and issue JWTs. realm or an 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.

However, 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 introduces sequential, cross-adapter dependencies:

StepSystemActionOutputDepends On
1FROST ServerCreate projectprojectId: "proj-123" → internal URLNone
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 URL → internal FROST URLprojectId from step 1
3Redpanda ConnectDeploy pipelinesPipeline writing to FROST projectURL from step 1, route from step 2

If step 3 fails after steps 1 and 2 succeeded, all previous changes must be rolled back to maintain consistency.

Key Constraints​

  • Adapters must remain simple command handlers without workflow knowledge
  • Rollback must guarantee eventual consistency across all involved systems
  • The solution must scale to additional backends (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./Stellio, MinIO, ...) without modifying existing adapters
  • Saga state must survive process restarts
  • The existing resultTopic mechanism in ConfigEvent must be preserved

Example Scenario​

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. "Zählerdaten Stadtwerke" (Dataspace-ID: ds-stadtwerke-zaehler)

Contains two pipelines:

  1. DB-Pipeline: Reads meter master data from a database, transforms to OGC STA entities, writes to FROST Server
  2. MQTT-Pipeline: Receives meter readings via MQTT, transforms to OGC STA Observations, writes to FROST Server

Provisioning requires FROST project creation → 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 creation → Redpanda pipeline deployment, in strict order.

Why Not Choreography?​

An initial Choreography-Based Saga proposal was evaluated. With ≥3 sequential steps, choreography leads to:

  1. Implicit workflow coupling: Each adapter must know its predecessor and successor in the chain
  2. Cascading compensation: Rollback propagates backwards through the entire chain; if any compensation fails, subsequent compensations never trigger
  3. Distributed routing logic: 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. adapter must distinguish FROST vs. Stellio predecessors and route compensations accordingly — violating Single Responsibility
  4. Bloated event payloads: The initial event carries configuration for all adapters, violating Information Hiding

A detailed analysis (last comment) documents these problems with sequence diagrams and code examples.

Checked Architecture Principles​

  • [full] Model-centric data flow
  • [full] Distributed architecture with unified user experience
  • [full] Modular design — Orchestrator is a separate module; adapters remain independent
  • [full] Integration capability through defined interfaces — Uses existing ConfigEvent/ConfigResultEvent contracts
  • [full] Open source as the default — Built on Apache 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.
  • [full] Cloud-native architecture — Stateless adapters, persistent saga state
  • [full] Prefer standard solutions over custom development — Saga Orchestration is a well-established pattern
  • [partial] Self-contained deployment — Adds one additional deployment unit (orchestrator module)
  • [full] Technological consistency to ensure maintainability — Same Java 21, Maven, 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. stack
  • [full] Multi-tenancy — Saga context carries dataspace/tenantTenantAn isolated organizational partition that owns Data pools, Datasets, Users, Groups, and Roles. All access rules exist within their Tenant, and the Tenant is the widest Scope of a Role. Currently, one Tenant corresponds to the Platform. identifiers
  • [full] Security by design — No new attack surface; uses existing 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. ACLs

Decision​

Introduce a dedicated config-adapter-orchestrator module that centrally manages multi-step provisioning workflows using the Orchestrated SagaSaga PatternA pattern for coordinating long-running, multi-step operations across several components without a distributed transaction. Each step has a compensating action; if a later step fails, the compensating actions of all previously completed steps are executed in reverse order. In CIVITAS/CORE it is used for provisioning workflows that require sequential, cross-adapter operations. pattern.

Architecture​

Portal Backend
│
│ dataspace.create.requested
▼
┌───────────────────────┐
│ Orchestrator │
│ │
│ Saga State (DB) │
│ Workflow Engine │
│ Compensation Mgr │
└──┬───────┬────────┬───┘
│ │ │
frost.project.│ apisix.route.│ redpanda.pipeline.
create/delete │ create/delete│ deploy/delete
│ │ │
▼ ▼ ▼
┌──────┐┌──────┐┌────────┐
│FROST ││APISIX││Redpanda│
│Adapt.││Adapt.││Adapt. │
└──────┘└──────┘└────────┘
│ │ │
│ result events │
└────────┴────────┘
│
▼
┌─────────────────────┐
│ Orchestrator │
│ (updates saga) │
└─────────────────────┘
│
│ dataspace.create.completed / .failed
▼
Portal Backend

Orchestrator Responsibilities​

The orchestrator is the only component with workflow knowledge:

ResponsibilityDescription
Workflow executionExecutes steps sequentially, passing outputs from one step as inputs to the next
Saga state managementPersists step results (resource IDs, URLs) to survive restarts
Data mappingBuilds adapter-specific commands from previous step outputs (e.g., projectId → 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. upstream URL)
CompensationOn failure, triggers DELETE commands for all successfully completed steps — in parallel
Status reportingReports final success/failure to the Portal Backend via a single result event

Adapter Responsibilities​

Adapters remain pure command handlers. They do not know whether a command is part of a saga, a manual operation, or a compensation:

Adapter receives: frost.project.create {name: "my-dataset"}
Adapter returns: frost.project.result {status: SUCCESS, resourceId: "proj-123",
resultData: {"projectId": "proj-123",
"baseUrl": "http://frost/v1.1/projects/proj-123"}}

Adapter receives: frost.project.delete {projectId: "proj-123"}
Adapter returns: frost.project.result {status: SUCCESS}

The adapter does not know the delete is a rollback. It is a regular delete operation.

Success Flow​

1. Backend → Kafka: dataspace.create.requested {backendType: "FROST", ...}
2. Orchestrator creates saga, persists state
3. Orchestrator → FROST Adapter: frost.project.create
4. FROST Adapter → Orchestrator: frost.project.result {projectId, baseUrl}
5. Orchestrator persists projectId, builds APISIX command
6. Orchestrator → APISIX Adapter: apisix.route.create {upstream: baseUrl, uri: ...}
7. APISIX Adapter → Orchestrator: apisix.route.result {routeId}
8. Orchestrator persists routeId, builds Redpanda command
9. Orchestrator → Redpanda Adapter: redpanda.pipeline.deploy {targetUrl, pipelineJson}
10. Redpanda Adapter → Orchestrator: redpanda.pipeline.result {pipelineId}
11. Orchestrator → Backend: dataspace.create.completed

Failure Flow (Redpanda Fails)​

Saga State after step 7:
FROST: SUCCESS (proj-123)
APISIX: SUCCESS (route-456)
Redpanda: FAILED (connection refused)

Orchestrator triggers parallel compensation:
→ apisix.route.delete {routeId: "route-456"}
→ frost.project.delete {projectId: "proj-123"}

Both compensations complete independently.
Orchestrator → Backend: dataspace.create.failed {reason, compensated: true}

Workflow Definition (Conceptual)​

workflows:
dataspace-create-frost:
trigger: "dataspace.create.requested"
condition: "payload.backendType == 'FROST'"
steps:
- name: create-frost-project
command: "frost.project.create"
input:
projectName: "{{payload.dataspaceName}}"
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.dataspaceId}}/*"
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}}"

Adding a new backend (e.g., Stellio) means adding a new workflow definition — no existing adapter code changes.

Required API Extension​

The ConfigResultEvent needs one additional field to carry structured result data:

// New field (backwards-compatible, nullable)
private Map<String, String> resultData;

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

Topic Structure​

TopicPublisherSubscriberPurpose
core.civitas.dataspace.create.requestedPortal BackendOrchestratorTrigger provisioning workflow
core.civitas.frost.project.createOrchestratorFROST AdapterCreate FROST project
core.civitas.frost.project.deleteOrchestratorFROST AdapterDelete FROST project (or compensate)
core.civitas.frost.project.resultFROST AdapterOrchestratorStep result
core.civitas.apisix.route.createOrchestratorAPISIXApache 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. AdapterCreate 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
core.civitas.apisix.route.deleteOrchestratorAPISIXApache 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. AdapterDelete 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 (or compensate)
core.civitas.apisix.route.resultAPISIXApache 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. AdapterOrchestratorStep result
core.civitas.redpanda.pipeline.deployOrchestratorRedpanda AdapterDeploy pipeline
core.civitas.redpanda.pipeline.deleteOrchestratorRedpanda AdapterDelete pipeline (or compensate)
core.civitas.redpanda.pipeline.resultRedpanda AdapterOrchestratorStep result
core.civitas.dataspace.create.completedOrchestratorPortal BackendSaga succeeded
core.civitas.dataspace.create.failedOrchestratorPortal BackendSaga failed (with compensation status)

Saga State Machine​

┌──────────┐
│ PENDING │
└────┬─────┘
│ first step started
▼
┌──────────┐
┌─────│EXECUTING │─────┐
│ └──────────┘ │
│ all steps done │ step failed
▼ ▼
┌─────────┐ ┌──────────────┐
│COMPLETED│ │ COMPENSATING │
└─────────┘ └──────┬───────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
┌────────────┐ ┌─────────────────────┐
│COMPENSATED │ │ COMPENSATION_FAILED │
│ (clean) │ │(manual intervention)│
└────────────┘ └─────────────────────┘

Limitation: Saga State Persistence​

The orchestrator requires persistent storage for saga state. Options:

StorageProsCons
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 DB neededHarder to query, operational complexity
PostgreSQL (shared with backend)Proven, transactional, queryableShared DB dependency
Dedicated DBFull isolationAdditional infrastructure

Recommendation: Use PostgreSQL (shared or dedicated), as the stack already includes it.

Consequences​

Benefits​

  • Adapters stay simple: No workflow knowledge, no compensation chains, no predecessor/successor awareness
  • Central workflow visibility: Saga state is queryable in one place — enables dashboards, debugging, and manual intervention
  • Parallel compensation: On failure, all successful steps are compensated simultaneously (not cascading)
  • Scalable to new backends: Adding 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. = new workflow definition, zero adapter code changes
  • Existing adapters unchanged: 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. and KeycloakKeycloakAn open-source Identity and Access Management (IAM) solution providing SSO and OAuth2/OpenID Connect flows. In CIVITAS/CORE it is used to authenticate users and issue JWTs. adapters continue to work as-is
  • Idempotent by design: Orchestrator can safely retry failed steps or compensations using existing adapter idempotency

Trade-offs​

  • Additional deployment unit: One new module (config-adapter-orchestrator) to build, deploy, and monitor
  • Saga state storage: Requires a database for persistence (PostgreSQL recommended)
  • Development effort: Orchestrator framework must be built (workflow engine, state management, compensation logic)
  • Single point of coordination: Orchestrator must be highly available; if it is down, no new sagas start (in-flight sagas resume on restart)

Affected Components​

ComponentChangeImpact
config-adapter-apiAdd resultData: Map<String, String> to ConfigResultEventBackwards-compatible, minimal
config-adapter-orchestratorNew moduleNew development
config-adapter-apisixNoneUnchanged
config-adapter-keycloakNoneUnchanged
config-adapter-applicationWire orchestrator moduleMinimal
Portal BackendSend dataspace.create.requested, receive completed/failedMinor
FROST Adapter (new)Implement as simple command handlerNew development
Redpanda Adapter (new)Implement as simple command handlerNew development

Alternatives Considered​

AlternativeDecisionRationale
Choreography-Based SagaRejectedAdapters accumulate workflow routing and compensation logic; cascading rollback breaks on partial failure; every new backend requires changes to existing adapters. See detailed analysis.
Portal Backend as Orchestrator (Option A)DeferredViable long-term, but requires significant backend changes for async 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. processing and saga state management. The dedicated module can be migrated into the backend later. A standalone orchestrator has the benefit of decoupled extensibility and maintenance
Process engine (Redpanda)Introduces heavyweight infrastructure dependency; overkill for the current number of workflows. Can be reconsidered if workflow complexity grows significantly.

Migration Path​

  1. Phase 1: Build config-adapter-orchestrator with saga state management and the FROST dataspace workflow
  2. Phase 2: Refactor FROST adapters as simple command handlers. Create Redpanda adapter as simple command handler
  3. Phase 3: Connect Portal Backend to orchestrator topics
  4. Phase 4 (optional): Add 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. workflow definition — existing adapters remain unchanged
  5. Long-term (optional): Migrate orchestrator logic into the Portal Backend if architecturally desired

References​