Approach: treat CDC records as upserts with canonical_key resolution, deterministic conflict rules (max event_timestamp, tie-break by source_priority then sequence_id), idempotency via storing last_applied_lsn/event_id per source-key, and soft-deletes via a deleted_flag + tombstone timestamp. We'll MERGE from a normalized staging table that contains one canonical_key per record and metadata: source, source_key, event_ts, source_seq, source_priority, op_type (I/U/D), event_id (unique per event).Example MERGE (T-SQL / ANSI-style):sql
MERGE INTO customers AS tgt
USING (
SELECT
canonical_key,
payload_name,
payload_email,
op_type,
event_ts,
source,
source_key,
source_seq,
source_priority,
event_id
FROM staging_cdc
WHERE processed = 0
) AS src
ON tgt.canonical_key = src.canonical_key
WHEN MATCHED AND
-- apply only if incoming event is newer or same-ts but higher priority/seq
(
src.event_ts > tgt.last_event_ts
OR (src.event_ts = tgt.last_event_ts AND src.source_priority > tgt.last_source_priority)
OR (src.event_ts = tgt.last_event_ts AND src.source_priority = tgt.last_source_priority AND src.source_seq > tgt.last_source_seq)
)
THEN UPDATE SET
name = CASE WHEN src.op_type = 'D' THEN NULL ELSE src.payload_name END,
email = CASE WHEN src.op_type = 'D' THEN NULL ELSE src.payload_email END,
deleted_flag = CASE WHEN src.op_type = 'D' THEN 1 ELSE 0 END,
deleted_ts = CASE WHEN src.op_type = 'D' THEN src.event_ts ELSE deleted_ts END,
last_event_ts = src.event_ts,
last_source = src.source,
last_source_key = src.source_key,
last_source_seq = src.source_seq,
last_source_priority = src.source_priority,
last_event_id = src.event_id,
updated_at = SYSUTCDATETIME()
WHEN NOT MATCHED BY TARGET
THEN INSERT (canonical_key, name, email, deleted_flag, deleted_ts, last_event_ts, last_source, last_source_key, last_source_seq, last_source_priority, last_event_id, created_at, updated_at)
VALUES (
src.canonical_key,
CASE WHEN src.op_type = 'D' THEN NULL ELSE src.payload_name END,
CASE WHEN src.op_type = 'D' THEN NULL ELSE src.payload_email END,
CASE WHEN src.op_type = 'D' THEN 1 ELSE 0 END,
CASE WHEN src.op_type = 'D' THEN src.event_ts ELSE NULL END,
src.event_ts,
src.source,
src.source_key,
src.source_seq,
src.source_priority,
src.event_id,
SYSUTCDATETIME(),
SYSUTCDATETIME()
);
Key points / reasoning:- Idempotency: each event carries a unique event_id (or LSN). We only apply when event is strictly newer per deterministic ordering; if same event is replayed, last_event_id prevents double-apply. Optionally keep an applied_events table (event_id PK) and check existence before merging for extra safety.- Deterministic conflict resolution: primary decision by event_ts; ties resolved by source_priority (preconfigured integer per source) then source_seq (monotonic sequence). This ensures same outcome regardless of arrival order.- Deletes: modeled as op_type='D', set deleted_flag and clear PII (or keep tombstone per policy). Soft deletes allow recovery and reconciliation.- Out-of-order events: ordering logic uses event_ts and sequence to accept late-arriving events only if they are newer per rules.- Recording source metadata: customers table stores last_source, last_source_key, last_source_seq, last_source_priority, last_event_ts, last_event_id for reconciliation. Additionally maintain an applied_events table:sql
CREATE TABLE applied_events (
event_id varchar PRIMARY KEY,
canonical_key varchar,
source varchar,
source_key varchar,
event_ts datetimeoffset,
applied_at datetimeoffset
);
Insert into applied_events within same transaction after MERGE to guarantee atomicity; use it to prevent reprocessing and to audit which source/event produced a value.Operational notes:- Normalize incoming keys to canonical_key in staging (lookup map or deterministic mapping rules).- Backfill source_priority and ensure monotonic source_seq per source (or use event_id monotonic).- Add checks to handle timezone skew and clock drift (use source-generated monotonic seq when possible).- For high-throughput, run dedupe and ordering in stream processing (Spark/Flink) and emit consolidated staging records for MERGE.