Change data capture
A Socratic walk-through of change data capture — reasoned out one step at a time, not lectured.
The question we started with
THE QUESTION #How do you copy every change a database makes without asking the database for anything?
You need a warehouse copy of an operational database that stays current within seconds. The obvious approach is to ask: run a query every so often and take what changed. And that approach, honestly examined, fails in ways that are worth sitting with — not because the query is slow, but because of what a query can and cannot see.
So the sharper question is this: is there a way to learn every change a database made without querying it for the data at all? It sounds like a trick. It is not, and the answer turns on something the database was already doing for its own reasons.
Reasoning it through
REASONING #Start by making the polling approach fail properly. You add an updated_at column and every minute ask for rows changed since last time. What do you miss?
Deletes, first. A deleted row has no updated_at to be greater than anything; it simply is not there. Your copy keeps it forever. You could add a soft-delete flag, but now you have changed the source application to serve the copier.
Second, intermediate states. If a row is updated three times inside your one-minute window, you see the final value and never learn there were three changes — and for an audit trail or anything that reacts to transitions, that is the very thing you cared about.
Third, and most subtly: the query competes with production. Every poll scans an index on a live transactional system.
Now the reframe. What does a database already do, without being asked, on every single write? Before it modifies a data page, it records the intended change durably in a sequential log — the write-ahead log in PostgreSQL, the redo log in Oracle, the binary log in MySQL. This is not a feature built for us; it exists so the database can recover from a crash and so replicas can follow the primary. But look at what it contains: every insert, every update, every delete, in commit order, with the values.
So the question answers itself. The changes need not be queried — they have already been written down. A capture process can read that log the way a replica does, and reconstruct the stream of changes without touching a single table.
Consider what that buys. Deletes appear, because a delete is a log record like any other. Every intermediate state appears, because the log records each write rather than a final state. Ordering is inherent. And the load on the source is that of one more replica, which the database was already designed to serve.
One gap remains. The log holds changes, not the current contents, and old segments are recycled. So a new consumer must first take a consistent snapshot of the existing rows, note the log position that snapshot corresponds to, and then begin streaming from exactly that position. Get that handover wrong — start streaming from a later position than the snapshot — and you silently lose every change in between. This snapshot-then-stream handover is where most of the real engineering in tools like Debezium lives.
The analogy
THE ANALOGY #Think of the difference between photographing a shop's shelves each evening and reading the till roll. The photographs tell you what is there now; comparing two of them tells you the net difference and nothing more. A tin that was sold and restocked looks untouched. A tin that was stolen looks like a tin you must have miscounted. The till roll, which the shop prints for its own accounting rather than for your benefit, records every transaction in the order it happened — sales, returns, voids — so from it you can reconstruct the shelves at any moment. But the roll only starts when you begin reading it, so on your first day you must still count the shelves once and note which receipt number that count corresponds to.
a till roll is printed on paper and stays where it is, whereas a database log is recycled aggressively — a capture process that stops for too long finds the records it needed already overwritten, and must fall back to a fresh snapshot.
Clarifying the model
THE MODEL #Three refinements matter.
The first is what the mechanism does not give you. The log records physical or row-level changes, not intent. You learn that a row's status went from 3 to 7; you do not learn that a customer cancelled an order. Deriving business meaning from a change stream requires knowing the source schema well, and that knowledge lives in the producing team — which is why unreviewed change capture is often described as reaching into another team's private state.
The second is the coupling that creates. The consumer now depends on the source's internal table structure, not on any interface the source team agreed to. A refactor that would have been invisible behind an API becomes a breaking change. This is a real cost of the technique, not a detail, and it is the main argument for producers publishing deliberate events instead.
The third is delivery semantics, and here it pays to be precise rather than reassuring. Log-based capture generally offers at-least-once delivery: after a crash the reader resumes from its last committed position and may replay records it already emitted. Exactly-once end to end is achievable, but only when the consumer cooperates — by making writes idempotent, usually keyed on the log position or the primary key, or by participating in a transactional protocol. A pipeline that assumes exactly-once because a vendor page mentions it will eventually double-count something.
Worth adding plainly: trigger-based capture, writing changes into an audit table from within the transaction, also works and avoids log-format dependence — at the cost of write throughput on the production path.
A picture of it
THE PICTURE #How to readStart at the parallelogram, the only place the application appears — everything below happens without it being asked. The two cylinders show the log fed by the database for its own durability, not for us. The first diamond is the handover most implementations get wrong: a new consumer must snapshot and record the exact log position it corresponds to before streaming. The second diamond is the standing hazard: if the reader falls behind the log's retention, the red node is the only honest outcome, and the back-edge shows recovery is a full re-snapshot, not a resume.
What became clearer
WHAT CLEARED #The trick is that you were never going to get every change by asking, because a query can only ever describe the present. The changes exist as a stream already, written by the database for its own recovery, and change data capture is simply the decision to read that record rather than to interrogate the tables. What you gain is deletes, intermediate states, ordering and low source load. What you accept is a dependence on someone else's internal schema, at-least-once delivery unless you do more work, and a hard failure mode when the reader falls behind the log's retention.
Where to go next
ONWARD #- How a snapshot is taken consistently without locking a busy table.
- Why change streams and event streams differ, and when to publish deliberate events instead.
- How a warehouse applies an update stream, and why merge-on-read and copy-on-write differ in cost.
Key terms
TERMS #| Term | What it means |
|---|---|
| Change data capture | observing and propagating every change made to a database, typically by reading its transaction log. |
| Write-ahead log | the durable sequential record a database writes before modifying data pages, used for crash recovery and replication. |
| Log position | a monotonic marker such as a log sequence number, used to resume reading exactly where a consumer left off. |
| Snapshot | a consistent one-time read of current rows, needed because a log holds changes rather than contents. |
| At-least-once delivery | a guarantee that no change is lost, but that some may be delivered more than once after a failure. |
| Idempotent apply | writing a change in a way that produces the same result if applied twice, which is what makes replay safe. |
Every term the collection defines is gathered in the glossary.