Skip to content

ADR-0002. CDC (Debezium) as the integration seam — producer-only Kafka

FieldValue
StatusAccepted
Date2026-03-15
Deciderscommerce-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 ApplicationKafkaComponent binds a producer only — and in practice does not call .send() anywhere in src/. A APPLICATION_KAFKA_CONSUMER binding 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 into TaxInfo by @nx/invoice (the authoritative downstream read source).

Consequences

ProsCons
Zero per-consumer emit code in commerce; new consumers attach without commerce changesConsumers 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 deliveryEventual consistency; consumers must be idempotent per-PK
Producer kept for future use without forcing it nowA bound-but-idle producer can confuse newcomers (documented here)

Alternatives Considered

OptionProsConsWhy rejected
Application-level Kafka produce per changeExplicit, curated payloadsHigh coupling; must instrument every write path in every serviceBrittle; defeats source-of-truth simplicity
Synchronous HTTP fan-out on each mutationImmediateTight runtime coupling; failure cascades; slow writesUnacceptable blast radius
Outbox table + relayTransactional guaranteesReinvents what Debezium gives for free over the WALDebezium 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_CONSUMER declared, unused)
  • src/services/merchant.service.ts (tax → metadata.tax → CDC → TaxInfo)

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.