---
id: WF-CRM-DQ-001
title: Detect duplicates after a committed change
trigger: A committed contact or organization change touches data used by matching.
product_area: crm
module: data-quality
revision: 1
status: draft
design_intent: proposed
implementation_evidence: observed
inference: []
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
implements:
  - CAP-CRM-DQ-001
  - CAP-CRM-DQ-002
  - CAP-CRM-DQ-003
  - CAP-CRM-DQ-007
  - CAP-CRM-DQ-008
  - CAP-CRM-DQ-011
moves:
  - STATE-CRM-DQ-001
  - STATE-CRM-DQ-002
crosses:
  - SVC-CRM-ENT-entities
  - SVC-CRM-DQ-duplicates
idempotency: >-
  Repeated passes replace comparison keys and upsert the same ordered pairs.
  Pair decisions and origin survive rescoring except for separate evidence-based
  reopening; evaluation timestamps and versions can still advance.
concurrency: >-
  Change handling batches up to one hundred records and writes all batch keys
  before neighbor lookup. Pair uniqueness prevents duplicate rows, but the
  pre-write decision read is not a commit-time review guard.
hops:
  - 'n': 1
    actor_or_service: Entities persistence
    action: Save the record and capture its changed-field event together.
    anchor: >-
      services-crm:modules/entities/services/persistence/contacts.ts:createContact
    seam: sync
    on_failure: Roll back record creation or editing with event capture.
    left_inconsistent: Neither record nor event partially commits.
    observer: caller.
    provenance: documented
    source: 'services-crm:docs/change-events.md#2. Capture'
  - 'n': 2
    actor_or_service: CRM change-events dispatcher
    action: >-
      Dispatch committed events to the registered duplicate handler, using
      after-commit nudges and the minute schedule.
    anchor: 'services-crm:aws/crm.change-events.ts:crmChangeEventStreams'
    seam: event
    on_failure: Keep unsent work recoverable after claim expiry.
    left_inconsistent: >-
      Saved records can have outdated duplicate evidence until dispatch
      succeeds.
    observer: Dispatcher logs.
    provenance: documented
    source: >-
      services-crm:docs/change-events.md#4. Dispatcher (one per macroservice,
      every stream)
  - 'n': 3
    actor_or_service: Duplicate-detection handler
    action: >-
      Ignore unrelated fields and import-marked events, then collect eligible
      record identities for the batch.
    anchor: >-
      services-crm:modules/duplicates/services/detection/duplicate-change-events.ts:detectForChangeEvents
    seam: queue
    on_failure: No independent retry is needed for normal filtering.
    left_inconsistent: Import markers bypass detection without the promised completion caller.
    observer: No direct observer.
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/unit/services/detection/duplicate-change-events.spec.js#answers
      no job id for the events the capture writes today, so every one of them is
      processed
  - 'n': 4
    actor_or_service: Duplicate detection
    action: >-
      Refresh keys, disqualify common values, compare eligible neighbors, score
      pairs and update their evidence and decisions.
    anchor: >-
      services-crm:modules/duplicates/services/detection/duplicate-detection.ts:detectForRecords
    seam: sync
    on_failure: Report every event failed when processing throws.
    left_inconsistent: >-
      Key replacement can precede a failed pair transaction; pairs and their
      regrouping roll back together.
    observer: Consumer log line per batch.
    provenance: observed
    source: >-
      services-crm:modules/duplicates/services/detection/duplicate-detection.ts:detectForRecords
  - 'n': 5
    actor_or_service: Duplicate groups
    action: >-
      Refresh affected connected groups with the pair changes and invoke the
      held attention seam.
    anchor: >-
      services-crm:modules/duplicates/services/persistence/duplicate-groups.ts:recomputeDuplicateGroups
    seam: sync
    on_failure: Abort the pair transaction if regrouping fails.
    left_inconsistent: Attention recipients receive no published item while registration is held.
    observer: 'Nobody: attention holders receive nothing.'
    provenance: observed
    source: >-
      services-crm:modules/duplicates/attention/open-duplicate-groups.attention.ts:OpenDuplicateGroups
  - 'n': 6
    actor_or_service: Change-event queue
    action: >-
      Redeliver failed handler messages and move exhausted delivery to
      dead-letter holding after five receives.
    anchor: 'services-crm:aws/crm.change-events.ts'
    seam: queue
    on_failure: >-
      Keep dead-letter messages for operator recovery under the documented
      retention and alarm policy.
    left_inconsistent: Keys or suggestions remain stale until recovery or another detection pass.
    observer: dead-letter alarm and operators.
    provenance: documented
    source: 'services-crm:docs/change-events.md#7. Failures and recovery'
branches:
  - >-
    Crossing the common-key cap triggers bounded rescoring of other open
    detected pairs relying on that value.
  - >-
    The survivor’s ordinary save emits the change that refreshes its duplicate
    evidence.
waits:
  - >-
    A lost dispatch nudge relies on the next minute schedule; this is a delivery
    cadence rather than a guaranteed end-to-end latency.
evidence:
  - >-
    services-crm at a8d1364f: modules/duplicates/services/detection/*.ts;
    services/persistence/duplicate-candidates.ts, duplicate-groups.ts,
    match-keys.ts; attention/open-duplicate-groups.attention.ts;
    docs/change-events.md; docs/merging/duplicate-detection.md §4 to §8.
controls: [TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-AZ-006, TC-CRY-004, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-IAM-001, TC-IAM-005, TC-IAM-006, TC-LOG-004, TC-LOG-005, TC-MON-001, TC-MON-002, TC-PI-001, TC-PI-002, TC-PI-003, TC-PI-004, TC-PI-005, TC-PRV-001, TC-PRV-002, TC-TEN-001, TC-TEN-002, TC-TEN-003]
---

# Detect Duplicates After A Committed Change

## What It Does

Background comparison follows a successful save rather than extending its transaction. It replaces comparison keys, recalculates eligible evidence and keeps affected review groups aligned with the pair writes.

## The Flow

```mermaid
flowchart TD
  H1["Save the record and capture its changed-field event together."]
  H2["Dispatch committed events to the registered duplicate handler, using after-commit nudges and the minute schedule."]
  H1 --> H2
  H3["Ignore unrelated fields and import-marked events, then collect eligible record identities for the batch."]
  H2 --> H3
  H4["Refresh keys, disqualify common values, compare eligible neighbors, score pairs and update their evidence and decisions."]
  H3 --> H4
  H5["Refresh affected connected groups with the pair changes and invoke the held attention seam."]
  H4 --> H5
  H6["Redeliver failed handler messages and move exhausted delivery to dead-letter holding after five receives."]
  H5 --> H6
```

## When Something Fails

Handler failure returns the batch for another delivery. Key refresh occurs before the pair-and-group transaction, so a failure does not prove that every pass write is undone. Exhausted events leave stale evidence until operator recovery or another change. Attention publication is currently held.

## Open Design Questions

Import-marked changes have no completion caller. Review-versus-detection races and the full pass transaction boundary need stronger implementation and proof before they are treated as guarantees.
