Skip to main content
Version: 2.0-rc2

Transactional Outbox Pattern

Problem​

The Portal Backend manages entities (users, groups, roles) and must synchronize 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 Configuration AdaptersConfiguration 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 original synchronous approach caused several issues:

  1. The Portal Backend published a ConfigEvent 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 waited for a ConfigResultEvent
  2. The database transaction remained open during the entire round-trip
  3. If the external system was slow or unavailable, the API request failed

This led to 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, and user-facing errors when downstream systems were temporarily unavailable.

Solution​

The 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. (ADR 030) replaces the synchronous request-reply flow with an asynchronous, eventually consistent model.

Core Principle​

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 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 the transaction has committed.

Flow Comparison​

Before: Synchronous (replaced)​

After: Asynchronous Outbox​

Synchronization State​

Each synchronized entity maintains an explicit state that reflects its progress in external system synchronization:

StateDescriptionAction
NOT_SYNCEDChange persisted locally, synchronization pendingAutomatic: outbox publisher sends event
SYNCEDChange successfully applied to external systemNone
FAILED_RETRIABLESynchronization failed, will be retriedAutomatic: retry with backoff
FAILED_PERMANENTSynchronization failed, requires manual interventionOperational: investigate and resolve

Key Characteristics​

What changes compared to the synchronous model:

  • API requests complete immediately after local database commit
  • Database transactions are short-lived and decoupled from external systems
  • The Portal Backend is resilient to temporary 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 outages
  • Synchronization with external systems is eventually consistent

What stays the same:

  • 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 are preserved
  • CloudEvents envelope structure is unchanged
  • Config Adapter interface remains the same

Failure Handling​

  • Temporary failures (network timeouts, service unavailability) are retried automatically until synchronization succeeds or a retry limit is reached
  • Permanent failures (invalid data, authorization errors) are marked explicitly and require operational attention
  • The original entity change is never rolled back -- the local state is authoritative

Database Changes​

The pattern requires two additions to the Portal Backend database:

  1. Sync state column on synchronized entities -- tracks NOT_SYNCED, SYNCED, FAILED_RETRIABLE, FAILED_PERMANENT
  2. Outbox table -- stores pending events for reliable publication 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.