~/software-design/change-data-capture-cdc-explained.md

Change Data Capture (CDC): Stream Every Database Change Without Touching Your App

4 min readNovember 03, 2026by Gorav Singal#change-data-capture#cdc#system-design
// TL;DR

Change Data Capture (CDC) streams every insert, update and delete out of your database as an ordered flow of events — so other systems (search index, cache, data warehouse, other services) can react to data changes, without the app doing anything extra. There are three ways to capture changes: QUERY-BASED (poll a table on updated_at — simple, but misses deletes, lags, and loads the DB), TRIGGER-BASED (DB triggers write to an audit table — catches everything but adds write overhead), and LOG-BASED (read the database's own replication log, the WAL in Postgres or binlog in MySQL, with tools like Debezium). Log-based is the winner: every change in commit order, near-zero load on queries, catches deletes, and the app doesn't change at all. The gotchas to plan for: ordering is per-table not global, delivery is at-least-once so consumers must be idempotent, the initial snapshot of a big table can be heavy, and schema changes ripple downstream. It is the backbone of modern data pipelines and cache invalidation.

Change Data Capture (CDC): Stream Every Database Change Without Touching Your App

Your orders live in Postgres. But you also need them in Elasticsearch for search, in Redis for a hot cache, in the data warehouse for analytics, and maybe pushed to another team’s service. The question every backend engineer eventually hits is: when an order changes in Postgres, how do all these other places find out? Change Data Capture is the clean answer, and once you understand it, you see it everywhere.

What CDC actually is

CDC means capturing every change — every INSERT, UPDATE and DELETE — that happens in your database, and turning it into a stream of events that other systems can consume. The elegant part is that, done properly, your application does not change at all. It keeps writing to the database exactly like before; CDC quietly watches the changes from the side.

Change Data Capture architecture: your app writes to the database as normal, and the database keeps its WAL or binlog recording every change in order. A CDC connector such as Debezium reads that log and emits to an event stream like Kafka or Kinesis, one topic per table. From there multiple consumers fan out: a search index, a cache refresh, a data warehouse, and other services. Why log-based CDC beats the alternatives: double-writing from the app is unreliable and polling the table on a timestamp misses deletes, hammers the DB and lags, whereas reading the database replication log gives a reliable ordered stream of every change with zero load on queries and no app change.

The key realisation: your database is already recording every change, in order, for its own replication and crash recovery. Postgres has the WAL (write-ahead log); MySQL has the binlog. CDC just reads that log. You are not adding a new source of truth — you are tapping into one the database already maintains for free.

Three ways to capture changes (only one scales)

Three ways to capture changes. Query-based polling runs SELECT WHERE updated_at greater than a timestamp every few seconds: simple with no special access, but it misses deletes, misses intermediate updates, loads the DB and lags. Trigger-based uses DB triggers to write change rows to an audit table: it catches everything in-DB but a trigger on every write adds overhead on the hot path and more DB logic to maintain. Log-based, the winner, reads the WAL or binlog directly with Debezium and friends: every change in commit order, near-zero load on queries, catches deletes and all updates, but needs log access and setup. The gotchas: ordering is per-table not global, delivery is at-least-once so consumers must be idempotent, the initial snapshot can be heavy, and schema changes ripple downstream.

People reinvent CDC badly all the time, so it is worth seeing the three approaches side by side:

  1. Query-based (polling). Run SELECT ... WHERE updated_at > ? every few seconds. It is simple and needs no special database access, but it is weak: it misses deletes entirely (a deleted row has no updated_at to find), it misses intermediate updates between two polls, it puts constant load on the DB, and it allways lags. Fine for a quick hack, not for serious use.

  2. Trigger-based. Put database triggers on your tables that write change rows into an audit table. This catches everything, but now every single write carries extra trigger work on the hot path, and you have more database logic to maintain and debug. It works, but it taxes your writes.

  3. Log-based (the real one). Read the WAL/binlog directly, usually with a tool like Debezium. This is the winner by a mile: you get every change in commit order, with almost zero load on your actual queries (you are reading a log, not running queries), it catches deletes and every update, and the application is completely untouched. The only cost is some setup and needing log access on the DB.

For anything real, log-based CDC is the right default.

The gotchas nobody mentions

CDC feels like magic, and that is exactly why teams get burnt. A few things to go in knowing:

  • Ordering is per-table (or per-partition), not global. Changes to a single table arrive in order, but if two different tables must stay mutually consistent, that is your problem to handle, not CDC’s.
  • Delivery is at-least-once. The same change can arrive twice (after a connector restart, for example). So your consumers must be idempotent — applying the same change twice should have no extra effect. This ties straight into the idempotency pattern; please do read that one also.
  • The initial snapshot can be heavy. Before streaming live changes, CDC usually takes a full snapshot of the existing table. For a huge table this is not light, so plan the first load carefully.
  • Schema changes ripple. If you drop or rename a column, every downstream consumer sees it. Version your events and coordinate schema changes — don’t just ALTER TABLE on a Friday evening and go home.
  • It is still eventually consistent. Consumers lag the source by a little. Same mindset as CQRS — design for the small delay.

None of these are blockers. They are simply the things that bite teh teams who assumed CDC was effortless. Know them up front, and CDC is honestly one of the best tools in the whole backend toolbox.

Where you will use it

  • Cache invalidation that actually works — when the row changes, the stream tells your cache to refresh. No more stale cache guesswork.
  • Search indexing — keep Elasticsearch in sync with the DB automatically.
  • Analytics / data warehouse — stream changes into the warehouse continuously instead of a nightly batch dump (see the Data Engineering course).
  • Microservice integration — one service’s DB changes become events other services react to. This is also the reliable publishing mechanism behind the Outbox pattern.
  • Zero-downtime migrations — stream from the old database to the new one while both run.

Takeaways

  • CDC streams every insert/update/delete out of your DB as ordered events, with the app untouched.
  • Prefer log-based CDC (read the WAL/binlog with Debezium) over polling (misses deletes, lags) or triggers (write overhead).
  • Plan for the gotchas: per-table ordering, at-least-once delivery (so consumers must be idempotent), heavy initial snapshot, and schema-change ripples.
  • It powers cache invalidation, search indexing, analytics pipelines, service integration and live migrations.

Part of the event-driven data patterns series: CQRS, Event Sourcing, the Outbox pattern, and idempotency. For the streaming side, the Apache Kafka and Data Engineering courses go much deeper.

Share

Related Posts

The Transactional Outbox Pattern: Never Lose an Event Again

The Transactional Outbox Pattern: Never Lose an Event Again

This is one of those patterns that looks like nothing until you get burnt once…

Event Sourcing, Explained: Store What Happened, Not Just the Final State

Event Sourcing, Explained: Store What Happened, Not Just the Final State

Here is a question I like to ask in interviews: your bank account shows a…

CQRS Pattern, Explained Simply: Split Your Writes From Your Reads

CQRS Pattern, Explained Simply: Split Your Writes From Your Reads

Most of us start our career with one model for everything. The same class, the…

The Circuit Breaker Pattern: Fail Fast, So One Dead Service Doesn't Kill Them All

The Circuit Breaker Pattern: Fail Fast, So One Dead Service Doesn't Kill Them All

Like the electrical switch in your home that it is named after, a circuit…

Message Queue vs Stream — RabbitMQ/SQS vs Kafka, and When to Use Which

Message Queue vs Stream — RabbitMQ/SQS vs Kafka, and When to Use Which

“Should we use RabbitMQ or Kafka?” is one of the most common — and most muddled…

Idempotency in Distributed Systems — Why Your Retry Just Charged the Customer Twice

Idempotency in Distributed Systems — Why Your Retry Just Charged the Customer Twice

Here is a bug that has cost real companies real money: a customer taps “Pay ₹50…

Latest Posts

How to Stay Valuable as a Software Engineer in the AI Era: A Practical Playbook

How to Stay Valuable as a Software Engineer in the AI Era: A Practical Playbook

In the previous article I argued that AI is not killing software engineering…

How to Review AI-Generated Code: A Checklist for Catching What the Model Missed

How to Review AI-Generated Code: A Checklist for Catching What the Model Missed

A few months back I approved a PR that was almost entirely AI-generated. It…

How to Debug AI-Generated Code: Understand First, Fix Last

How to Debug AI-Generated Code: Understand First, Fix Last

In the last article I wrote about reviewing AI code before it goes in. This one…

The Future of Software Engineering: What AI Changes, What Stays, and Where the Jobs Go

The Future of Software Engineering: What AI Changes, What Stays, and Where the Jobs Go

Every few weeks somebody sends me a scary headline — “AI will replace all…

The Transactional Outbox Pattern: Never Lose an Event Again

The Transactional Outbox Pattern: Never Lose an Event Again

This is one of those patterns that looks like nothing until you get burnt once…

Event Sourcing, Explained: Store What Happened, Not Just the Final State

Event Sourcing, Explained: Store What Happened, Not Just the Final State

Here is a question I like to ask in interviews: your bank account shows a…