Why must a sink applying CDC change events use idempotent upserts rather than replaying source statements?
answer
- the read position is committed periodically
- a restart re-emits the last window
- the event carries the whole row, not a diff
- avoid anything relative such as qty = qty - 1
- merge the after-image on the primary key
basics
~20 sA CDC pipeline redelivers events after any restart, so a replayed insert collides on the key and a replayed relative update double-counts. Writing each event's after-image as an upsert keyed on the primary key makes redelivery a no-op.
solid answer
~50 sChange data capture commits its read position periodically, not once per event, so a crash, a task rebalance or a manual replay re-emits every change since the last committed position. At-least-once is what you actually ship, and the sink is where that becomes harmless. The safe shape is to take the event's **after-image** — the full row state after the change — and apply it as an upsert keyed on the source primary key: insert if absent, overwrite every column if present, and apply a delete as a delete-if-exists. Applying the same event five times then leaves the row exactly as the source has it. What breaks is anything relative or append-shaped: `qty = qty - 1`, an unguarded insert into a fact table, a counter, an outbound email. Each of those needs its own dedup key. Idempotency alone is not enough either — without per-key ordering a replay can still lay an old image over a new one.
code
sql · 8 lines-- Idempotent apply of one change event's after-image
MERGE INTO customers t
USING (SELECT :id AS id, :email AS email, :updated_at AS updated_at) s
ON t.id = s.id
WHEN MATCHED THEN
UPDATE SET email = s.email, updated_at = s.updated_at
WHEN NOT MATCHED THEN
INSERT (id, email, updated_at) VALUES (s.id, s.email, s.updated_at);go deeper
Know that a change event carries the full row after the change, and that the loader can be handed the same event twice. Be able to say why an upsert on the primary key is safer than a plain insert.
Explain why position commits are periodic and what that implies about delivery, then write the merge that applies an after-image. Name at least one operation that is not idempotent and the dedup key that fixes it.
Show that you have operated this: replays you cause yourself are the common case, not crashes. Discuss dedup keys for append-only targets, their index and retention cost, and side effects the sink cannot make idempotent.
Own the platform position: make at-least-once plus idempotent sinks the default contract rather than chasing exactly-once per pipeline, and be able to justify that against the cost of transactional sinks and the freedom to rebuild targets.
## Why a CDC stream redelivers A change-data-capture pipeline reads the source database's transaction log and emits one change event per row change. To resume after an interruption it periodically records the log position it has processed — every few seconds or every N events, never once per event, because a durable write per event would destroy throughput. If the process dies between emitting an event and recording that position, everything after the last recorded position is emitted again when it restarts. The same thing happens on a task rebalance, on a connector restart, when the sink retries a failed batch, and whenever an operator deliberately rewinds to re-load a target. So a CDC feed is **at-least-once** in practice: every change appears at least once, and after any interruption a window of recent changes appears twice. This is not a defect you can configure away in the general case. Even where a stack offers transactional or exactly-once delivery into a specific sink, the moment you rebuild a target, add a second consumer, or replay a backfill you re-introduce duplicates on purpose. A pipeline whose correctness depends on nothing ever being delivered twice is a pipeline that cannot be operated. ## What an idempotent apply looks like An operation is idempotent if applying it repeatedly leaves the same state as applying it once. Change events make this unusually easy, because a well-formed event carries the **after-image**: the complete state of the row after the change, not a description of the edit. That lets the sink's write mean "make the row with this key look exactly like this", which is inherently repeat-safe: ```sql MERGE INTO customers t USING (SELECT :id AS id, :email AS email) s ON t.id = s.id WHEN MATCHED THEN UPDATE SET email = s.email WHEN NOT MATCHED THEN INSERT (id, email) VALUES (s.id, s.email); ``` Deletes follow the same rule: `DELETE FROM customers WHERE id = :id` affects one row the first time and zero rows every time after, which is exactly what you want. An insert event is handled by the same merge as an update, which is why sink code usually does not branch on the operation type at all except to separate deletes. ## Operations that are not idempotent Four shapes break under redelivery: - **Relative updates.** `SET qty = qty - 1` reads the current value, so each replay subtracts again and the sink silently drifts from the source. Take the absolute value from the after-image instead. - **Appends without a dedup identity.** Loading change events into a history or fact table with a plain `INSERT` writes the duplicate as a real row. The fix is a unique identity per change — the pair of primary key and source log position is unique for every change the source ever made — and either a merge that only inserts when not matched, or a dedup step in staging. - **Aggregates computed by counting events.** "Orders today" derived by counting change events overcounts after a replay; derive it from the deduplicated current-state or history table instead. - **External side effects.** An email, a webhook, a payment call or a search-index write are not undone by the sink's idempotency. Each needs its own idempotency key carried in the event and checked at the far end. ## Idempotency is not ordering A common half-answer stops here. Idempotent writes make *repetition* harmless; they do nothing about *reordering*. If two changes to the same row are processed by two workers, the older after-image can be written last and the sink holds a stale value indefinitely — and unlike a duplicate, nothing later corrects it. The two properties have to be built together: route or partition change events by primary key so one row's history stays on one ordered path, collapse a batch to the newest event per key before merging, and where reordering is still possible, guard the merge with the source log position so an older image cannot overwrite a newer one. ## What this buys you operationally An idempotent sink turns a class of incidents into non-events. A connector that fell over at 3am and re-emitted an hour of changes needs no intervention. A target table that was corrupted by a bad deploy can be dropped and rebuilt by replaying the stream. A second sink can be added and caught up from an earlier position without coordinating with the first. That operational freedom, not just crash safety, is the real reason experienced engineers insist on it before they discuss delivery guarantees at all. ## What interviewers listen for The strong answer names at-least-once as the practical guarantee, explains *why* (periodic position commits), shows the after-image upsert, names at least one non-idempotent shape and its dedup key, and then volunteers that ordering is a separate problem. The weak answer claims the pipeline is exactly-once and stops.
- If the target is an append-only history table rather than a current-state table, how do you make the load idempotent?Give every change a stable identity — the source primary key plus the source log position is unique per change — and either merge with an insert-when-not-matched clause on that identity, or deduplicate in staging before inserting. Keep an index on the identity, and bound it: prune the dedup window to something safely wider than the largest replay you would ever perform.
- Where can a duplicate still be observed even though every sink write is idempotent?In side effects and in anything derived by counting events. A webhook, an email, a payment call or a downstream API write happens again unless it carries its own idempotency key. Metrics computed from the raw stream — events per minute, rows loaded — also overcount after a replay; derive business counts from the deduplicated table instead.
- Does an exactly-once sink remove the need for idempotent writes?Not in practice. It narrows the crash case, but the replays you cause deliberately — rebuilding a target, adding a consumer, re-snapshotting a table, rewinding after a bad deploy — reintroduce duplicates by design. Idempotent writes are what make those routine operations safe, so most teams keep them even when a transactional sink is available.
A change event is a photograph of the row, not a diff. Pinning the same photograph to the wall twice leaves the wall looking identical; following the same instruction "move it two inches left" twice does not.
saying these in an interview costs you the question
- Says CDC is exactly-once so the sink needs no deduplication
- Replays raw INSERT statements and treats duplicate-key errors as the dedup strategy
- Applies deltas from change events instead of the after-image
- Believes idempotent writes also fix out-of-order arrival
- Claims committing the read position after every event removes replay