# ADR-0003: Transactional Outbox für Kafka-Events statt Dual-Write

* Status: Angenommen
* Datum: 2026-08-17 (nachträglich dokumentiert; Entscheidung war mit Commit `9714741` wirksam)

## Kontext und Problemstellung

Ordering, Approval, Receiving und Invoicing müssen fachliche Zustandsänderungen sowohl in der
eigenen PostgreSQL-Datenbank persistieren als auch als Kafka-Event für andere Module (u. a.
`procurex-audit`, `procurex-notification`) veröffentlichen. Ein naiver Ansatz — erst die
Datenbank committen, danach das Event auf Kafka publizieren — kann bei einem Absturz zwischen
beiden Schritten zu einem inkonsistenten Zustand führen (DB-Commit ohne Event oder Event ohne
DB-Commit).

## Entscheidungstreiber

* Zwischen Ordering → Approval → Receiving → Invoicing besteht eine fachliche Kette, die auf
  zuverlässiger Event-Zustellung beruht (siehe Confluence Seite 09/10).
* Kein verteilter Transaktionsmanager (XA/2PC) über Kafka und PostgreSQL hinweg gewünscht — zu
  hohe Komplexität für ein Lernprojekt und in der Praxis auch dort meist vermieden.
* Jedes der vier Module hat sein eigenes Schema und darf nicht auf ein fremdes Schema zugreifen
  (siehe ADR-0001) — die Lösung muss pro Modul unabhängig funktionieren.

## Betrachtete Optionen

* Dual Write: DB-Commit, danach Kafka-Publish, ohne weitere Absicherung.
* Transactional Outbox: Event wird in derselben DB-Transaktion wie die fachliche Änderung in eine
  Outbox-Tabelle im jeweiligen Modul-Schema geschrieben; ein separater Relay-Prozess liest die
  Outbox-Tabelle und publiziert die Events an Kafka.
* Change Data Capture (z. B. Debezium) auf den Modul-Schemas.

## Entscheidung

Transactional Outbox, umgesetzt in `procurex-common` als wiederverwendbare Basis
(`AbstractOutboxEvent`, `AbstractOutboxRelay`) und je Modul eine eigene Outbox-Tabelle im
Modul-Schema plus ein `@Scheduled`-Relay.

## Begründung

* Der Outbox-Eintrag entsteht in derselben lokalen DB-Transaktion wie die fachliche Änderung —
  damit ist der Schritt „Zustand persistiert“ atomar mit „Event zur Veröffentlichung vorgemerkt“.
* Kein zusätzlicher Infrastrukturbaustein nötig (im Gegensatz zu Debezium/CDC), passt zur
  bewusst schlanken Infrastruktur dieses Lernprojekts.
* Die gemeinsame Basisklasse in `procurex-common` vermeidet Code-Duplikation über die vier
  publizierenden Module hinweg, ohne die Schema-Trennung zu verletzen (jedes Modul hat weiterhin
  seine eigene Tabelle).

## Konsequenzen

* Positiv: kein Event-Verlust bei Absturz zwischen DB-Commit und Kafka-Publish; live gegen einen
  echten Kafka-Broker verifiziert für sechs Topics (siehe `procurex-app/README.md`, Abschnitt
  „Events“).
* Negativ: Events werden nicht synchron mit dem HTTP-Response veröffentlicht, sondern mit der
  Latenz des Scheduled-Relays (Polling-Intervall) — für den aktuellen Anwendungsfall akzeptabel,
  da keine Near-Realtime-Anforderung besteht.
* Bewusst noch offen: kein interner Consumer außer `procurex-audit` und `procurex-notification`
  bislang, daher keine konsumentenseitige Dead-Letter-Queue umgesetzt — nur die Namenskonvention
  `<topic>.dlq` ist dokumentiert (siehe Confluence Seite 10).

## Weitere Informationen

Siehe Confluence Seite 10 „APIs, Events, Daten und Security“ (CONFIRMED), Abschnitt
„Umsetzungsnachweis Security & Events“, sowie `procurex-common` (Outbox-Basisklassen) und
`procurex-app/README.md`.
