ADR 015: Asynchronous Outbox for Config Adapter Synchronization
Date: 2026-01-15
Status: Reviewed
Decision Makers: @luckey @cr0ssing @jonasfrz
Context
The Portal Backend manages e.g. users, groups, and roles and synchronizes these changes to external systems (e.g. 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.) via the Config Adapter.
Current flow:
- A CRUD request is received by the Portal Backend
- The entity is saved in the database
- A ConfigEvent is published to 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.
- The Portal Backend waits for a ConfigResultEvent from the ConfigAdapterConfiguration AdapterThe component that consumes data models on behalf of platform components that cannot consume them directly, and configures the component accordingly. In the secrets management flow, the Configuration Adapter is the sole component that resolves Vault references into concrete credentials.
- The database transaction stays open while waiting
- On success, the external ID is stored and the transaction is committed
- On failure or timeout, the transaction is rolled back
This flow guarantees strong consistency but causes:
- Long-running transactions
- Tight runtime coupling to 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. and the Config Adapter
- User facing errors when external systems are slow or unavailable
Checked Architecture Principles
- [full] Model-centric data flow – Events stored alongside domain changes ensure data consistency
- [full] Distributed architecture with unified user experience – Async pattern enables better service independence
- [full] Modular design – Clear separation between Portal Backend and Config Adapter
- [full] Integration capability through defined interfaces – Uses CloudEvents standard via 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] Open source as the default – Pattern based on open-source best practices
- [full] Cloud-native architecture – Outbox patternTransactional Outbox PatternA pattern for reliably publishing events after a database change: Entity changes and their corresponding outbox events are persisted in a single database transaction. A separate publisher process reads the outbox table and publishes events to Kafka after the transaction has committed. is well-suited for distributed cloud environments
- [full] Prefer standard solutions over custom development – Transactional Outbox is a proven pattern
- [full] Self-contained deployment – Each service manages its own outbox independently
- [full] Technological consistency to ensure maintainability – Consistent event-driven architecture
- [full] Multi-tenancy – Sync state tracked per entity enables 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. isolation
- [full] Security by design – Transactional guarantees prevent data loss and inconsistencies
Decision
We replace the synchronous request–reply flow with an asynchronous outbox-based pattern.
The Portal Backend no longer waits for the Config Adapter. Changes are committed immediately and synchronized asynchronously.
What Changes
1. Asynchronous Processing
- CRUD operations return after database commit
- No blocking while waiting for 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. or Config Adapter responses
2. Transactional Outbox
- Entity change and outgoing event are stored in the same transaction
- Events are published to 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. after commit
- Event delivery is reliable
3. Explicit Sync State
Each synchronized entity maintains an explicit synchronization state that reflects its progress and outcome when applying changes to external systems:
NOT_SYNCED – change persisted locally, synchronization pending
SYNCED – change successfully applied externally
FAILED_RETRIABLE – synchronization failed and will be retried
FAILED_PERMANENT – synchronization failed and requires intervention
The synchronization state represents the authoritative view of external consistency for each entity and is persisted alongside the domain data.
4. Result Handling
- The Config Adapter publishes result events after processing configuration changes
- Result events are consumed asynchronously by the Portal Backend
- Synchronization state is updated based on the reported outcome
- Temporary failures transition the entity into a retriable failure state
- Persistent failures are marked explicitly and handled operationally
- The original database change is never rolled back
New Flow (Simplified)
API request
→ save entity (NOT_SYNCED)
→ save outbox event
→ commit
→ response returned
Outbox publisher
→ publish ConfigEvent
Config Adapter
→ apply change
→ publish result
Result listener
→ update sync state
(SYNCED / FAILED_RETRIABLE / FAILED_PERMANENT)
Consequences
- API requests complete immediately after local persistence, resulting in faster and more predictable response times.
- Database transactions remain short-lived and are no longer coupled to external systems.
- The Portal Backend becomes resilient to temporary outages or slowdowns of 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. or the Config Adapter.
- Synchronization with external systems becomes eventually consistent by design.
Failures during synchronization are handled explicitly:
- Temporary failures are retried automatically until synchronization succeeds or a defined limit is reached.
- Persistently failing changes are marked accordingly and require operational attention.
- Failed synchronizations do not roll back the original change
This approach improves system stability and observability but introduces the need for operational processes to monitor and resolve failed synchronizations.
Alternatives
- Keep synchronous validation → rejected (scalability and reliability)
Migration Notes
- Add sync state column
- Introduce outbox table and publisher
- Remove synchronous waiting and rollbacks
- Keep 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. topics and event formats
See Also
- Ticket: https://gitlab.com/civitas-connect/civitas-core/civitas-core-v2/civitas-core-platform/-/issues/730
- Existing Portal ↔ Config Adapter integration
- Transactional Outbox PatternTransactional Outbox PatternA pattern for reliably publishing events after a database change: Entity changes and their corresponding outbox events are persisted in a single database transaction. A separate publisher process reads the outbox table and publishes events to Kafka after the transaction has committed.
- CloudEvents Specification
- ADRArchitecture Decision RecordA document capturing an architecture decision. Each ADR has a stable identifier and short title and is managed through four lifecycle states: Proposed, Accepted, Deprecated and Superseded. 013: Use cloudevents Standard for Bus based Configuration Communication