In a CDC feed, what should a sink do with a delete event?
answer
- there is no after-image to write
- the target keeps growing if you skip them
- one option removes the row, one marks it
- analysts usually still want the row
- a null payload for a key means something specific
basics
~20 sApply it by key: either remove the row, or mark it deleted with a flag and a timestamp so history survives. Ignoring delete events leaves rows in the target that no longer exist in the source, and counts drift upward forever.
solid answer
~60 sA delete change event carries the row's key and its state *before* the delete; there is no after-image, because the row is gone. The sink has three reasonable responses. **Physical delete** keyed on the primary key mirrors the source exactly and is naturally idempotent — the second apply affects zero rows — but the row's history is lost and the target can no longer tell "deleted" from "never seen". **Soft delete** writes an `is_deleted` flag and a `deleted_at` timestamp; this is the usual warehouse choice because analysts still want the row, and because keeping the key lets the loader remember how far it had progressed for it. **Append to history** records the delete as another versioned row. Whichever you pick, every downstream query must filter soft-deleted rows or your numbers include them. Some connectors also emit a follow-up record carrying the key with a null payload — a tombstone — which a keyed store uses to drop the key entirely; a sink must recognise it rather than reading it as "set all columns to null".
code
text · 8 linesop=delete
key = {id: 7}
before = {id: 7, email: "[email protected]", tier: "gold"}
after = null
optional follow-up record for keyed stores:
key = {id: 7}
value = null <- tombstone: the key itself may be droppedgo deeper
Be able to say that a delete event names the row's key, has no after-image, and must be applied — either by removing the row or by flagging it — or the target keeps rows the source no longer has.
Compare physical and soft delete on their real costs: lost history and lost per-key bookkeeping on one side, a filter every consumer must remember on the other. Explain what a tombstone record is for.
Talk about detection and prevention: reconciliation against the source to catch dropped deletes, a view that enforces the soft-delete filter, and the interaction between physical deletes and replay-resurrected rows.
Set the policy across targets — whether the platform's contract is physical or soft deletion, who owns the filtering, and whether source teams should be asked to stop hard-deleting at all in favour of a status column.
## What a delete event contains When a row is deleted at the source, a log-based capture emits an event whose operation is a delete, carrying the row's key and — as far as the source log recorded it — the values the row held before it disappeared. There is no after-image: nothing exists afterwards to describe. That asymmetry is the whole reason delete handling gets its own design decision, because every other operation hands the sink a complete picture of what the row should now look like, and this one does not. How much of the before-image you get depends on what the source log was configured to record. Some engines log the full previous row, others only the key. A sink that needs the old column values — to close out a versioned history row, for instance — has to check that the source is configured to provide them rather than assuming. ## Option 1: physical delete ```sql DELETE FROM customers WHERE id = :id; ``` The target then matches the source row-for-row, which is what an operational replica or a cache wants. It is idempotent for free: applying it again affects zero rows. The costs are that history is destroyed, that the target cannot distinguish a deleted row from one that never arrived, and — the operational sting — that the sink forgets any per-row bookkeeping it kept, such as the source log position reached for that key. That matters because a replayed pre-delete update will then find no row, take the insert path, and resurrect a row the source deleted. ## Option 2: soft delete ```sql UPDATE customers SET is_deleted = TRUE, deleted_at = :ts WHERE id = :id; ``` This is the default in analytical targets. Nothing is destroyed, so a report over last quarter still sees the customer who churned yesterday; the key survives, so any position guard on the row keeps working; and re-inserting the same key later is a normal upsert that clears the flag. The price is discipline: every consumer must filter `is_deleted = FALSE`, and the day someone forgets, deleted rows silently join a total. The standard mitigation is to expose a view that applies the filter and to point consumers at the view rather than the table. ## Option 3: append to a history table If the target is an append-only record of changes, the delete is just another versioned row with an operation column set to delete. Nothing is overwritten and downstream models reconstruct current state as "the newest version per key that is not a delete". This is the most faithful representation and the most expensive to query. ## Tombstones Separately from the delete event itself, some CDC connectors emit an additional record that carries the row's key and a **null payload**. This is a tombstone, and it exists for keyed stores that retain only the latest value per key: seeing a null value for the key tells such a store the key may be removed entirely rather than kept forever with its last known value. Two consequences for a sink author: the sink must recognise a null payload as "this key is gone", not decode it as a row whose every column is null; and tombstone emission is usually optional, so a downstream keyed store that relies on it will grow without bound if it is turned off. ```text op=delete key={id:7} before={id:7,email:"[email protected]"} after=null (optional follow-up) key={id:7} value=null <- tombstone ``` ## The failure of ignoring deletes The most common mistake is a loader that only handles inserts and updates, either because it was written against a source where deletes were thought impossible, or because the target's merge logic simply had no delete branch. The symptom is quiet and slow: the target keeps every row it has ever seen, row counts drift above the source's, and aggregates — active customers, open orders, inventory value — are inflated by rows the business deleted months ago. It is usually caught by a reconciliation job that compares row counts and checksums per key range against the source, which is worth having regardless. ## Hard deletes at the source versus in your pipeline A related design conversation: many teams stop the source from hard-deleting at all, using a status column instead, so the CDC feed only ever sees updates. That removes this whole class of problems but is a source-application decision you may not control. If deletes do reach your pipeline, handle them explicitly and write the test that proves it — a delete at the source, then an assertion that the target no longer counts the row. ## What an interviewer wants to hear That delete events carry a before-image and no after-image; that the sink chooses between physical and soft delete for reasons, not by accident; that soft delete needs a filter everywhere; and that ignoring deletes inflates the target silently. Mentioning tombstones and what they are for is the differentiator at this level.
- What is the operational risk of choosing soft deletes?Every consumer has to filter the flag, and one that forgets silently counts deleted rows. Expose a view that applies `is_deleted = FALSE` and point consumers at the view rather than the base table, and cover it with a test that deletes a row at the source and asserts the view's count drops.
- How would you detect that a loader has been quietly dropping delete events for months?Reconcile against the source: compare row counts and per-key-range checksums on a schedule, and alert when the target's count exceeds the source's. The signature of dropped deletes is a target that only ever grows relative to the source, with the gap widening steadily rather than appearing all at once.
saying these in an interview costs you the question
- Ignores delete events because the loader only handles inserts and updates
- Decodes a null-payload tombstone as a row with all columns null
- Uses soft deletes but lets consumers query the base table unfiltered
- Assumes the delete event always carries every previous column value
- Physically deletes rows and is then surprised when replays resurrect them