ADR 015: Asynchronous Outbox for Config Adapter Synchronization
Date: 2026-01-15 Status: Proposed 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. Keycloak) 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 Kafka
- The Portal Backend waits for a ConfigResultEvent from the ConfigAdapter
- 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 Kafka 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 Kafka
- [full] Open source as the default – Pattern based on open-source best practices
- [full] Cloud-native architecture – Outbox pattern 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 tenant 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 Kafka or Config Adapter responses
2. Transactional Outbox
- Entity change and outgoing event are stored in the same transaction
- Events are published to Kafka 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 Kafka 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 Kafka 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 Pattern
- CloudEvents Specification
- ADR 013: Use cloudevents Standard for Bus based Configuration Communication