Home / data / data-reconciliation-patterns
data-reconciliation-patterns
Patterns for reconciling Salesforce data with external systems: count-level, field-level, and record-level reconciliation, external ID upsert patterns, Change Data Capture for delta detection, and soft-delete tombstone strategies.
Open the skill folder on GitHub
Data Reconciliation Patterns
This skill activates when a Salesforce integration has produced a data discrepancy — record counts differ, field values drift, or records exist in one system but not the other. It provides a layered reconciliation strategy (count, field, record) plus the upsert and delta-detection primitives needed to close those gaps reliably.
Before Starting
Gather this context before working on anything in this domain:
- Identify the external ID field used as the join key between Salesforce and the source system. If none exists, one must be created before upsert-based reconciliation is possible.
- Confirm whether the integration uses Bulk API 2.0, REST API, or a middleware platform — the reconciliation approach differs for each.
- Establish a baseline: what is the expected record count in Salesforce, and what was the timestamp of the last successful sync? These two values anchor every reconciliation check.
Core Concepts
The three levels of reconciliation form a diagnostic ladder. Start at count level, then drill down only when counts diverge.
Count-Level Reconciliation
Count-level reconciliation compares the number of records in Salesforce against the source system. In Salesforce, use SELECT COUNT() FROM Object__c WHERE <filter> — the aggregate returns a single integer with no row limit. Cross-reference this against the equivalent count query on the source side. A mismatch means records were not created, were deleted, or were filtered differently.
Count-level reconciliation is cheap and fast. Run it after every bulk load job before treating a sync as complete. Note that COUNT() counts only the rows the running user can see. Over the REST/SOAP API it always respects that user's sharing, so the count is trustworthy only when the integration user holds View All Data on the object (or the count comes from the Tooling API for metadata counts) — that is a permission, not an execution mode. Run the same query from Apex and the default mode is gated by the apiVersion in the class's .cls-meta.xml, not by the org's release: at 67.0+ (Summer '26) SOQL defaults to user mode and a class with no sharing keyword runs with sharing, so a batch reconciliation class that returned every row when pinned to 58.0 silently under-reports once it is saved at 67.0. Declaring that class without sharing does not restore the old count: the operation-level default is set by the apiVersion, not by the sharing keyword, so a count that must see every row opts back in on the statement with WITH SYSTEM_MODE plus a // reason: comment. Canonical table: