Skip to content

ADR 0004: Use broker-neutral events and a transactional outbox ​

  • Status: Accepted
  • Date: 2026-08-06

Context ​

Modules need a way to trigger asynchronous work and communicate completed business facts without becoming coupled to one another's repositories. Kafka may be introduced later for independently deployed consumers, replay, or throughput, but adding it before module boundaries exist would add operational complexity without resolving internal coupling.

Publishing directly to a broker after committing a PostgreSQL transaction can lose an event if the process fails between the two operations.

Decision ​

Define broker-neutral, versioned domain/application events. Event contracts do not expose Kafka, Valkey, Gin, pgx, HTTP DTO, or generated repository types.

Use direct in-process calls for immediate decisions and simple reads. Use in-process event delivery only where loss on process failure is acceptable. Use a PostgreSQL transactional outbox for reliable asynchronous effects:

text
business transaction
├── update module-owned data
└── insert outbox record
         │
         ▼
      worker relay
         ├── local handler initially
         └── Kafka publisher later

Kafka will be implemented as an adapter behind the publisher/consumer interfaces when a concrete operational requirement exists.

Event conventions ​

  • Events describe completed business facts.
  • Names use past tense and a schema version, for example device-record.retired.v1.
  • Envelopes contain a unique event ID, type, version, occurrence time, tenant ID where applicable, correlation ID, and causation ID where available.
  • Producers and consumers are idempotent.
  • Payload evolution is backward compatible or introduces a new version.

Consequences ​

  • Reliable events are committed atomically with business state.
  • A worker process is required to relay outbox records.
  • Retry, poison-message, retention, and observability policies must be implemented.
  • Kafka can be added without changing module business logic.
  • The backend outbox is distinct from the frontend IndexedDB mutation outbox.

Alternatives considered ​

Add Kafka immediately ​

Rejected until a concrete consumer or scale requirement justifies its operational cost.

Use Valkey pub/sub for reliable business events ​

Rejected because pub/sub does not provide the required durable transactional delivery guarantee.

Publish after committing the business transaction ​

Rejected for reliable events because it creates a dual-write failure window.

Ovahol developer documentation