Change Data Capture#
Problem statement (interviewer prompt)
Design a pipeline that streams every change in a production OLTP database to a data warehouse, a search index, and a cache invalidator - without dual-writes and without losing events on failure. Cover CDC, the outbox pattern, schema evolution, and exactly-once semantics.
flowchart LR
APP[App] --> DB[(Primary DB)]
DB -. WAL / binlog .-> CDC[CDC tailer<br/>Debezium / DMS]
CDC --> BUS[Kafka / Pub-Sub]
BUS --> CACHE[Cache invalidator]
BUS --> SEARCH[Search index]
BUS --> DWH[Data warehouse]
BUS --> AUDIT[Audit / event log]
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class APP,CACHE,SEARCH service;
class DB,DWH datastore;
class BUS,AUDIT queue;
class CDC compute;
flowchart TB
subgraph Sources[Source DBs]
PG[(Postgres - logical WAL,<br/>wal2json / pgoutput)]
MY[(MySQL - binlog ROW)]
MG[(MongoDB - oplog / change streams)]
DY[(DynamoDB Streams)]
OR[(Oracle - LogMiner / GoldenGate)]
SQ[(SQL Server CDC tables)]
end
subgraph Capture[Capture]
DEB([Debezium connectors])
DMS[AWS DMS / GCP Datastream]
MAX[Maxwell / Canal]
SNAP[Initial snapshot + ongoing tail]
PUB[(Replication slot / publication)]
end
subgraph Bus[Streaming Bus]
KAF[[Kafka topics<br/>one per table]]
SR[Schema Registry<br/>Avro / Protobuf]
COMPACT[Log compaction<br/>by PK]
end
subgraph Sinks[Sinks / consumers]
ES[(Search index<br/>Elasticsearch)]
CACHE[Cache invalidator]
DWH[(Snowflake / BigQuery / Redshift)]
LAKE[(Data lake / Iceberg / Hudi)]
ML[Feature store]
AUDIT[Audit immutable log]
INV[Inverse: SCD2 dim]
OUT[[Outbox-based event publisher]]
end
subgraph Patterns
OUTB[[Outbox pattern<br/>transactional event emission]]
DEDU[Dedup at sink via offset/LSN]
BACK([Backfill bootstrap])
EVOL[Schema evolution]
DLQ[[DLQ on transform errors]]
SAGA[Sagas powered by CDC]
end
PG --> DEB
MY --> DEB
MG --> DEB
DY --> DMS
OR --> DMS
SQ --> MAX
DEB --> KAF
DMS --> KAF
KAF --> SR
KAF --> COMPACT
KAF --> ES
KAF --> CACHE
KAF --> DWH
KAF --> LAKE
KAF --> ML
KAF --> AUDIT
OUTB --- DEB
SNAP --> KAF
DLQ --- DEB
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class DMS,MAX,SNAP,SR,COMPACT,CACHE,INV,DEDU,EVOL,SAGA service;
class PG,MY,MG,DY,OR,SQ,PUB,ES,DWH,LAKE,ML,AUDIT datastore;
class KAF,OUT,OUTB,DLQ queue;
class DEB,BACK compute;
CDC mechanics#
- Log-based: tail the DB's WAL/binlog → low overhead, captures every change.
- Trigger-based: row triggers write to shadow table → higher overhead, simpler.
- Polling: timestamp/CDC column scan → loses deletes, high lag.
Snapshot + tail#
- On first connect, snapshot table(s) to topic with
__op=r(read). - Then attach to log position; events arrive as
c/u/d(create/update/delete).
Ordering & exactly-once#
- Per-table topic → ordered by PK with
partition.key = pk. - Sinks track
offsetor(lsn, op)for idempotent upserts. - Schema registry enforces backward-compat changes.
Real uses#
- Cache invalidation: bust Redis key on row change.
- Search index sync: ES via Debezium sink.
- Analytics: Postgres → Kafka → Snowflake/BigQuery (near-real-time).
- Service decomposition: extract a microservice consuming legacy DB CDC.
- Audit log: append-only journal.
Pitfalls#
- Schema changes (DDL) need handling; some connectors halt on ALTER.
- Initial snapshot of huge tables - use chunked snapshot mode.
- Replication slots in Postgres can pin WAL and fill disk if consumers lag.
- Connector failure semantics - Debezium "at-least-once" with offsets; design sinks for replay.
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Cache strategies | cache-aside, read/write-through, eviction | caching-strategies |
HLD |
Pub/Sub & message brokers | topics, consumer groups, delivery semantics | pub-sub-pattern |
HLD |
Leader/follower replication | sync/semi-sync/async replication, failover | replication-leader-follower |
HLD |
LSM vs B-Tree engines | WAL, memtable, SSTables, compaction | storage-engines-lsm-btree |
HLD |
Distributed transactions | 2PC, TCC, sagas, outbox/inbox | distributed-transactions |
HLD |
Change Data Capture | WAL/binlog tailing, outbox publishing | change-data-capture |
LLD |
Testing strategy | pyramid, doubles, TDD, contracts | testing-strategy |
LLD |
Immutability | immutable types, persistent collections | immutability |
Quick reference#
Why CDC#
- Single source of truth for "what changed" without dual-writes.
- Dual-writes (write DB + write to Kafka in app) are unsafe - atomicity gap.
- CDC + outbox closes the gap.
Outbox pattern (paired with CDC)#
- App writes
domain_rowandoutbox(event_id, payload)in same DB tx. - CDC tails outbox and publishes events.
- Mark or delete outbox rows post-publish.
This decouples app from broker availability.
Bootstrap modes#
- Snapshot-only (one-time export).
- Snapshot + streaming (initial backfill + ongoing).
- Streaming-only (rebuild from log retention).
- Chunked / parallel snapshot for huge tables.
Sink design rules#
- Idempotent upserts (
MERGEorON CONFLICT). - Track last applied
(lsn, op_seq)per partition. - Handle tombstones (deletes) → either physical delete or soft-flag.
Watch out#
- Postgres replication slot retention: pin WAL → disk full risk. Set
max_slot_wal_keep_size. - MySQL binlog format: must be ROW (not STATEMENT) for usable CDC.
- DDL events: most tools have schema history topic; coordinate downstream.
- PII redaction at capture (column filter) if downstream is less trusted.
Refs#
- Debezium docs and connector reference.
- Martin Kleppmann: "Turning the database inside out", "Online event processing".
- Confluent blog series on CDC.
- AWS DMS, GCP Datastream docs.
FAQ#
What is change data capture?#
Change data capture (CDC) streams every insert, update, and delete from a database to downstream consumers like a data warehouse, search index, or cache, usually by tailing the write-ahead log.
What is the difference between CDC and dual writes?#
Dual writes update the DB and a message broker in application code, which is prone to inconsistency on failure. CDC reads the DB's own log, so it is atomic with the commit and never loses events.
How does Debezium work?#
Debezium is a Kafka Connect source that tails Postgres WAL, MySQL binlog, or MongoDB oplog and publishes each row change as a Kafka record. It guarantees at-least-once delivery with offset tracking.
When should I use CDC vs the outbox pattern?#
Use CDC when you want to capture every change without app code changes. Use the outbox pattern when domain events are richer than row changes, like business events with computed fields and identities.
What problems does CDC solve?#
CDC eliminates dual-write inconsistency, enables real-time analytics, cache invalidation, search index updates, and event-driven microservices without a transactional outbox table.
Related Topics#
- Event Sourcing and CQRS: CDC produces the change events that event-sourced systems and CQRS read models consume
- Pub/Sub Pattern: CDC pipelines publish database change events to pub/sub topics for downstream consumers
- Replication: Leader-Follower: CDC typically reads from the database replication log (WAL/binlog) used by leader-follower replication
Further reading#
Curated, high-credibility sources for going deeper on this topic.