The first version of a data reconciliation system almost always has the same failure mode: it works correctly, then gets deployed to production, and immediately generates hundreds of duplicate alerts. The same discrepancy is reported every time the job runs. Operators learn to ignore the alert channel within a week. The system that was supposed to improve data reliability becomes noise.
The root cause is the same in almost every case: the comparison engine was built as a stateless script. Each run produces a fresh list of discrepancies with no awareness of what previous runs found. Without state management, every run is the first run.
Making a reconciliation system idempotent - ensuring that running it multiple times produces the same observable outcome as running it once - requires both deterministic comparison logic and persistent state that survives between runs. This piece covers the patterns that make that work in production.
Why Idempotency Is the Right Frame
Idempotency is a property typically discussed in the context of API calls and database writes: calling an endpoint or executing a write multiple times should have the same effect as calling it once. The same principle applies to reconciliation runs.
A reconciliation run is idempotent when: running it twice on unchanged source data produces zero new alerts, no duplicate discrepancy records, and the same state in the tracking store as running it once. The run reads, compares, and updates state - but only creates new signals where something actually changed.
This property requires two things. First, the comparison engine must produce deterministic output from the same input. The same two records compared twice should produce the same list of discrepancies. Second, the discrepancy tracker must distinguish between "new discrepancy" and "discrepancy we already know about."
The Stable Discrepancy Identifier
The mechanism that enables idempotent state management is a stable identifier for each discrepancy. This identifier must be deterministic from the discrepancy's properties and stable across runs.
A practical stable identifier is a hash of the record's comparison key, the field name, and optionally the values from each source. The comparison key ensures the identifier is unique to a specific record. The field name ensures it is unique to a specific field on that record. Including the values is optional but useful if you want different values for the same field on the same record to generate distinct discrepancy records.
import hashlib
import json
def discrepancy_id(comparison_key, field_name, value_a=None, value_b=None):
"""
Generates a stable identifier for a discrepancy.
Omit value_a and value_b to make the ID value-independent
(same discrepancy regardless of the specific values).
"""
payload = {
"key": str(comparison_key),
"field": field_name,
}
if value_a is not None or value_b is not None:
payload["values"] = [str(value_a), str(value_b)]
canonical = json.dumps(payload, sort_keys=True)
return hashlib.sha256(canonical.encode()).hexdigest()[:16]
Using this identifier, the tracker upserts on each run. SQLAlchemy provides upsert support via its ORM and core layers for managing the discrepancy state table. Redis is useful as a deduplication cache for the current run's processed record set.: if a discrepancy with this identifier exists, update its last_seen timestamp. If it does not exist, create a new record and queue an alert.
The Discrepancy Lifecycle
A production discrepancy tracker manages four states:
Open: The discrepancy was detected and has not yet been resolved. Each subsequent run that still finds the discrepancy updates last_seen without creating a new alert.
Acknowledged: An operator has reviewed the discrepancy and flagged it as known. Acknowledged discrepancies do not generate repeat alerts even if they persist across runs. This is the key escape valve for cases where the discrepancy is a known acceptable difference (two systems intentionally representing the same entity differently) rather than an error.
Resolved: A run completed without finding the discrepancy. The tracker marks it resolved and can optionally send a resolution notification.
Escalated: The discrepancy has been open beyond a configured threshold and has been escalated to a higher-urgency channel or a human-review queue.
The state machine transitions:
- New detection -> Open (alert fires once)
- Open, found again -> Open (update last_seen, no new alert)
- Open, not found -> Resolved (optional resolution notification)
- Open, beyond threshold -> Escalated
- Acknowledged, found again -> Acknowledged (no alert, no state change)
- Acknowledged, not found -> Resolved
def process_discrepancy(tracker, disc_id, field_name, val_a, val_b):
existing = tracker.get(disc_id)
if existing is None:
tracker.create(disc_id, field_name, val_a, val_b, status="open")
alert(disc_id, field_name, val_a, val_b)
elif existing["status"] == "open":
tracker.update_last_seen(disc_id)
elif existing["status"] == "acknowledged":
pass # known, no action needed
Handling Resolved Discrepancies
Detecting resolution requires comparing the set of open discrepancies against the set of discrepancies found in the current run. Any open discrepancy that does not appear in the current run's findings has resolved.
def resolve_cleared(tracker, current_run_ids):
open_discrepancies = tracker.get_by_status("open")
for disc in open_discrepancies:
if disc["id"] not in current_run_ids:
tracker.mark_resolved(disc["id"])
This logic must account for partial comparisons. If the current run only compared a subset of records (incremental mode), discrepancies for records not included in this run's comparison should not be marked resolved - they were simply not checked. The resolution check should only run against discrepancies whose records were included in the current run's comparison scope.
One practical approach: tag each discrepancy with the comparison scope (date range, record ID range, or full) at creation time, and only attempt resolution for discrepancies whose scope is covered by the current run.
Comparison Determinism
Idempotent alerting is only possible if the comparison engine itself is deterministic. The same two records compared twice must produce the same discrepancy output. Non-determinism typically enters through:
Floating-point comparison without a tolerance. Direct equality comparison of float values can produce inconsistent results when the same underlying value is represented with different floating-point precision across systems. Always use a tolerance threshold for float comparisons and enforce it consistently.
Timestamp handling without normalization. Comparing timestamps from two systems without normalizing to UTC and stripping subsecond precision that one system tracks and the other does not will produce spurious discrepancies on repeated comparisons of the same records.
Null handling inconsistency. If your comparison treats null-in-A vs value-in-B as a discrepancy in some contexts but not others, the same pair of records can produce different results on different runs depending on which code path evaluates them.
Document and enforce the comparison rules for each field type before writing the state management layer. Non-determinism in comparison logic makes the state management layer impossible to reason about.
The Value of Acknowledged State
The acknowledged state deserves emphasis because it is the pattern that makes reconciliation systems sustainable in environments where not every discrepancy is an error.
Two systems that represent the same customer entity will sometimes have legitimately different representations of certain fields. A billing system rounds currency amounts; the CRM does not. An analytics system uses a different timezone for date fields than the operational system. These are not errors - they are design decisions. But they will surface as discrepancies in any comparison.
Without acknowledged state, operators must either tune the comparison to exclude these fields (reducing coverage) or accept that those discrepancy alerts will fire on every run forever (producing noise). Acknowledged state gives you a third option: detect the discrepancy once, review it, and mark it as an accepted difference that should not generate alerts on subsequent runs but should still appear in audit reports.
The guide on data reconciliation system design at 137Foundry covers the full architecture including the discrepancy tracker schema and how the state machine integrates with the comparison engine and scheduling layer.
Production Deployment Checklist
Before deploying a reconciliation system to production:
- Verify comparison logic is deterministic: run it twice on the same data and compare the output byte-for-byte
- Test resolution detection with a controlled discrepancy that you clear between runs
- Verify that acknowledged discrepancies do not generate alerts on re-detection
- Test partial-scope runs to confirm they do not incorrectly resolve out-of-scope discrepancies
- Use Prefect or a similar orchestration tool to handle scheduling, retry policies, and run health monitoring. Set up monitoring on the reconciliation job itself: how long each run takes, how many records were compared, how many discrepancies were found
- Define alert thresholds for escalation before any real discrepancies appear
A reconciliation system that generates reliable, low-noise, actionable signals is worth significantly more than one that technically detects everything but has trained its operators to filter the channel. Idempotency is the engineering property that makes the difference between the two.
Top comments (0)