Architecture Decision Records
This document captures the why behind major decisions in ch-olap-pipeline. Future contributors should read this first.
ADR-001 — Real CDC instead of polling
Status: Accepted
Context: Original reference project used polling (SELECT ... WHERE updated_at > last_run). In heterogeneous OLTP environments with DELETE-heavy workloads, polling is expensive, lossy, and operationally painful.
Decision: Use Change Data Capture via Debezium source connectors reading native database logs (WAL for PostgreSQL, binlog for MySQL, CDC job for SQL Server).
Consequences:
- Pro: low latency (< 1s per event), DELETE detection, low source DB overhead
- Pro: replayable event log in Kafka
- Con: more components (Kafka, Kafka Connect, Debezium)
- Con: source DB needs CDC-aware config (replication slots, binlog format, CDC jobs)
ADR-002 — Apache Kafka as the event bus
Status: Accepted
Context: Need a durable, replayable, partitioned event bus between Debezium and the analytical sink.
Decision: Use Apache Kafka in KRaft mode (no ZooKeeper). Single-node for dev, multi-node for production.
Consequences:
- Pro: industry standard, tooling-rich, schema registry compatible
- Pro: KRaft removes ZK operational complexity
- Con: JVM footprint (~1-2GB minimum), not suitable for very small VPS
ADR-003 — ClickHouse as primary sink
Status: Accepted
Context: Need a column-oriented analytical store with high-throughput ingestion and low-latency query. Alternatives: BigQuery (cloud-locked), Snowflake (cloud-locked, expensive), Iceberg (lake-format, not query engine), Druid (operationally heavy).
Decision: ClickHouse is the first-class sink. Other sinks are roadmap items, not v1 promises.
Consequences:
- Pro: native Kafka Engine, Materialized Views, ReplacingMergeTree for dedup
- Pro: self-hostable, on-prem friendly
- Con: less mature for non-Kafka ingestion paths
ADR-004 — Single partition per topic for order guarantee
Status: Accepted
Context: CDC events for one table must be processed in commit order, otherwise UPDATE arrives before INSERT and the analytical state is wrong.
Decision: Force topic.num.partitions=1 per source table. Scale horizontally by adding more tables or more source databases, not by splitting a single table's topic.
Consequences:
- Pro: simple ordering guarantee per row
- Con: limits per-topic throughput to one consumer
- Mitigation: tune
task.max.queue.sizeand add more Connect workers for more parallel tables
ADR-005 — ReplacingMergeTree + FINAL for deduplication
Status: Accepted
Context: Even with ordered topics, operators need idempotent ingestion and recovery from out-of-order arrivals during connector restarts or partition reassignment.
Decision: Use ClickHouse ReplacingMergeTree(version) with a version column derived from Debezium LSN / binlog position. User-facing queries use the FINAL modifier.
Consequences:
- Pro: idempotent, safe for retries and out-of-order events
- Pro:
FINALgives correct read semantics automatically - Con:
FINALadds a per-query merge cost — acceptable for OLAP workloads, not ideal for high-frequency small reads
ADR-006 — Modular connector configs, no hardcoded database list
Status: Accepted
Context: The project is positioned to grow beyond PostgreSQL, MySQL, and SQL Server. Hardcoding "supported databases" in core code would block extension.
Decision: Each source database is a self-contained JSON config + docs under deploy/kafka-debezium/connectors/. Adding a new source = dropping a new JSON + writing a section in the README. No code changes to the core pipeline.
Consequences:
- Pro: clean community contribution model
- Pro: each adapter has independent config knobs
- Con: per-DB setup steps live in markdown, not enforced by code — relies on contributors to keep docs accurate
ADR-007 — Heartbeat intervals for replication slot keepalive
Status: Accepted
Context: PostgreSQL logical replication slots are persistent — if a connector dies and the slot is unused, WAL accumulates until disk is full. Operators routinely hit this in production.
Decision: Mandate heartbeat.interval.ms=10000 in PG connector config. Document the last-known LSN check in troubleshooting.
Consequences:
- Pro: slot stays active, no silent disk fill
- Con: minor traffic overhead (one heartbeat per 10s)
ADR-008 — No exposure of Kafka / Connect to public internet
Status: Accepted
Context: Kafka and Kafka Connect have no built-in auth in default config. Exposing them publicly = data exfiltration vector.
Decision: Default compose binds Kafka/Connect/ClickHouse to localhost only. Public access via reverse proxy is opt-in and requires explicit auth (SASL_SSL / HTTPS).
Consequences:
- Pro: secure default
- Con: operators must consciously add TLS / auth for remote access — documented in production checklist
ADR-009 — Source DBs use low-privilege CDC users
Status: Accepted
Context: The CDC user is the only thing standing between the pipeline and a misconfiguration dropping a production table.
Decision: Document and require per-DB CDC user with:
- PostgreSQL:
REPLICATIONprivilege +SELECTonly on replicated tables - MySQL:
REPLICATION SLAVE,REPLICATION CLIENT,SELECTon replicated tables - SQL Server: membership in
db_ownerfor the source DB only (CDC requirements), or more restrictive if vendor allows
Consequences:
- Pro: blast radius limited if credentials leak
- Con: per-DB setup steps are not optional — documented in install guide
ADR-010 — Documentation as a first-class deliverable
Status: Accepted
Context: The reference project this is built on shipped minimal docs. Operators adopting it hit the same wall repeatedly.
Decision: Every feature ships with:
- install steps
- verification commands
- troubleshooting section
- known limitations
Docs are reviewed with the same rigor as code.
Consequences:
- Pro: project is actually usable by people who did not build it
- Con: PRs feel heavier — but that is the right tradeoff