ADR-0002. CDC (Debezium) as the integration seam — producer-only Kafka
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-03-15 |
| Deciders | commerce-team, platform-team |
| Supersedes | — |
Context
- Commerce is the catalog/tenant source of truth. Many services need its data: search (index products/merchants/categories), inventory (seed locations + items), pricing (init fares), taxation (provision tax groups), invoice (derive
TaxInfo). - Application-level Kafka produce would require commerce to know every downstream contract, emit a topic per change shape, and keep emit code in sync with every catalog mutation across many services — high coupling and easy to miss a write path.
- Postgres already records every change in the WAL. Debezium can capture it generically.
Decision
Use CDC (Debezium) as the primary integration seam. Commerce's only obligation is to write correctly to Postgres; Debezium tails the WAL and publishes one topic per table (CDCKafkaTopics / CdcTables in @nx/core, e.g. public.Merchant, public.ProductVariant).
- Downstream consumers (search/inventory/pricing/taxation/invoice) own their CDC consumer wiring.
- Commerce's
ApplicationKafkaComponentbinds a producer only — and in practice does not call.send()anywhere insrc/. AAPPLICATION_KAFKA_CONSUMERbinding key is declared but no consumer is wired. - The only CDC consumer commerce itself runs is
ApplicationCdcComponent(from@nx/search, WORKER role) to sync its own DB into Typesense. - Merchant tax info exemplifies the pattern: written to
Merchant.metadata.tax, captured by CDC, and upserted intoTaxInfoby@nx/invoice(the authoritative downstream read source).
Consequences
| Pros | Cons |
|---|---|
| Zero per-consumer emit code in commerce; new consumers attach without commerce changes | Consumers couple to table schemas — column renames are breaking |
| No risk of "forgot to emit on this write path" | CDC adds Debezium/Kafka Connect as operational dependencies |
| WAL ordering + offset replay gives durable, replayable delivery | Eventual consistency; consumers must be idempotent per-PK |
| Producer kept for future use without forcing it now | A bound-but-idle producer can confuse newcomers (documented here) |
Alternatives Considered
| Option | Pros | Cons | Why rejected |
|---|---|---|---|
| Application-level Kafka produce per change | Explicit, curated payloads | High coupling; must instrument every write path in every service | Brittle; defeats source-of-truth simplicity |
| Synchronous HTTP fan-out on each mutation | Immediate | Tight runtime coupling; failure cascades; slow writes | Unacceptable blast radius |
| Outbox table + relay | Transactional guarantees | Reinvents what Debezium gives for free over the WAL | Debezium already covers it |
References
packages/core/src/common/cdc/tables.ts(CdcTables)packages/core/src/common/kafka/topics.ts(CDCKafkaTopics)src/components/kafka/component.ts(producer bound, no.send())src/common/keys.ts(APPLICATION_KAFKA_CONSUMERdeclared, unused)src/services/merchant.service.ts(tax →metadata.tax→ CDC →TaxInfo)