The question this answers
My search index and my database disagree. What repairs that, and who runs it?
Reconciliation guarantees a bounded divergence window: any drift between the source of truth and a derived store is detected within one reconciliation cycle and repaired within one repair cycle. It does not prevent drift, and it does not guarantee the derived store is correct at any given instant — only that it cannot be wrong indefinitely without someone knowing.
Everything below is bought to hold this sentence. "Strongly consistent" with no scope attached is a slogan, not a guarantee — read what it actually covers, and what it explicitly does not.
No component can detect its own drift. A search index holding a stale document has no local evidence of the problem: the document is well-formed, was written successfully, and every health check passes. Drift is a relation between two stores, so it is knowable only to something that reads both. That is the structural argument for reconciliation as a distinct component — nothing already in the system is positioned to notice.
A node knows its own state and the messages that arrived. Everything else is inference from evidence that was already stale. "B has not replied in five seconds" is knowledge; "B is down" is a decision — and usually the bug.
Why drift is certain
Every mechanism that keeps a derived store in step with its source can fail without raising an error, and there are at least six such mechanisms in a typical system.
A missed event: the consumer was down past the stream’s retention, or a message went to a dead-letter queue and was never replayed. A bug in the projection: it computed the wrong value for a class of inputs, consistently and successfully. A partial failure: the source committed and the propagation did not, or the propagation succeeded and the source rolled back. A manual write: an operator fixed something in the derived store during an incident. A duplicate: a non-idempotent update applied twice. An out-of-order delivery: two updates to one key applied in the wrong sequence, leaving the older value.
None of these produces an error, and there is no rate at which they stop happening. Which gives the design conclusion: since drift is inevitable, the repair mechanism is part of the system, not a response to a bug. A system with derived state and no reconciliation is not a system that has avoided drift; it is a system that has not measured it.
- A missed event past retention, or one that died in a dead-letter queue.
- A projection bug that computes the wrong value successfully.
- A partial failure between the source commit and the propagation.
- A manual write to the derived store during an incident.
- A duplicate applied to a non-idempotent update.
- Out-of-order delivery leaving an older value in place.
The four steps
Compare. Read both stores and identify the differences. For small datasets, a full comparison. For large ones, compare cheap summaries first — a checksum or count per key range, refining only into ranges that disagree, which is the same idea as [[merkle-trees]] and [[anti-entropy]] applied to a service boundary rather than to replicas.
Classify. Not every difference is drift. A record written two seconds ago and not yet projected is expected lag, not an error. Reconciliation must know the acceptable staleness window and exclude anything inside it, or it produces constant false positives and gets muted — which is the most common way a reconciliation job dies.
Repair. Rewrite the derived value from the source. Repair should be idempotent and rate-limited: a job that discovers two million discrepancies and fixes them all at once will saturate the store it is repairing, and a repair job that causes an incident does not get to run again.
Alert on the delta. This is the step teams skip and it is the most valuable. The size of the delta is a health signal for the whole derivation pipeline. A steady small number is normal. A step change means something broke. A slow rise means a systematic bug is accumulating. Repairing silently hides the failure it is repairing, so a reconciliation job that fixes without reporting is a system that has learned to conceal its own defects.
1for range in key_ranges: # 1. COMPARE, cheaply first2 if checksum(source, range) == checksum(derived, range):3 continue # identical — skip the detail4 for key in keys_in(range):5 s, d = read(source, key), read(derived, key)6 if s == d: continue7 8 # 2. CLASSIFY <-- skipped => false positives => alert muted => job dies9 if age(s.updated_at) < ACCEPTABLE_STALENESS:10 metrics.inc('reconcile.within_lag_window'); continue11 if d is MISSING: kind = 'missing'12 elif s is MISSING: kind = 'orphan'13 else: kind = 'divergent'14 15 # 3. REPAIR — idempotent, and rate limited so it cannot cause an incident16 rate_limiter.acquire()17 write(derived, key, s)18 metrics.inc('reconcile.repaired', kind=kind)19 20# 4. ALERT ON THE DELTA <-- skipped => the job silently hides the bug it fixes21metrics.gauge('reconcile.delta_total', repaired_count)22metrics.gauge('reconcile.oldest_drift_seconds', oldest_drift)23if repaired_count > BASELINE * 3:24 page('derivation pipeline degraded — delta stepped up, not a one-off')Three shapes of difference, three different repairs
Treating every discrepancy identically is a common and expensive mistake, because the three shapes have different causes and different correct responses.
Missing in the derived store. The source has it, the derivation does not. Usually a dropped or unprocessed event. The repair is to write it, and the diagnostic question is whether the event was lost or merely never consumed — the answer determines whether this recurs.
Divergent values. Both have the record, the values differ. This is the interesting case: it points at a projection bug, an out-of-order application, or a second writer. Repairing it without asking which is how a projection bug survives for months, quietly repaired every night.
Orphan in the derived store. The derived store has a record the source does not. Either a delete was not propagated, or — much more importantly — the derived store holds data that exists nowhere else, in which case it is not derived at all and deleting the orphan destroys the only copy. An orphan is the signal that your ownership map is wrong, and it should be surfaced rather than automatically cleaned up.
| Likely cause | Repair | What a rising count means | |
|---|---|---|---|
| Missing in derivedtypical | Dropped event, unconsumed backlog, DLQ | Write it from the source | The delivery path is losing events — fix the pipeline, not the data |
| Divergent valuetypical | Projection bug, out-of-order apply, second writer | Overwrite from the source — and investigate | A systematic bug that nightly repair is concealing |
| Orphan in derivedassumption | Unpropagated delete — or data that exists only here | Surface it; delete only after confirming it is derived | Your ownership map is wrong; something is authoritative that you labelled derived |
Designing it in, and the objection it always meets
The objection is predictable: "if the pipeline were correct we would not need this." The answer is that the pipeline is a distributed system, and every distributed system loses messages, duplicates them and reorders them under conditions you do not control. Reconciliation is not compensation for a poor implementation; it is the mechanism that makes an at-least-once, eventually-consistent derivation *operable* — the same relationship that a filesystem check has to a journalling filesystem, or that an inventory count has to a stock system that is otherwise perfectly good.
Design decisions worth making explicitly. Scope: full sweep or incremental over a recent window? Incremental is cheap enough to run every few minutes and catches the common cases; full sweeps are expensive and catch old drift, so run both at different cadences. Load: reconciliation reads both stores, which is real traffic — run it off replicas and rate-limit it. Authority: it must be unambiguous which side wins, or the job has no defined behaviour; if you cannot say, the problem is [[source-of-truth]], not reconciliation. Ownership: a job nobody owns gets muted the first time it is noisy and is never re-enabled.
Two implementation notes that matter more than they look. Comparing a moving system produces false positives from records that changed *during* the comparison; snapshot both sides at a consistent point, or compare with a window and tolerate churn. And the job must be safe to run twice at once, because it will be.
The reconcile step of an incident
This is where the module closes back on [[detect-contain-recover]]. Reconciliation is the fourth step of that spine, and having it as a standing component is what makes the step executable rather than aspirational. During an incident you do not want to be writing a comparison script; you want to run the existing one bounded to the incident window.
That is the practical argument for building it before you need it, and it is stronger than the correctness argument. A reconciliation job written under pressure, against stores that are still recovering, by an engineer who has been awake for six hours, is exactly the tool most likely to make the incident worse — a repair with no rate limit, no staleness window and no dry run.
The mature version has a bounded mode: reconcile only entities modified between these two timestamps, report first, repair on confirmation. That single capability converts the vaguest step of incident response into a command someone can run.
Key points
- Derived state will drift; six independent mechanisms cause it and none of them raises an error.
- Four steps: compare, classify, repair, alert on the delta.
- Alerting on the delta is the step teams skip — silent repair conceals the bug it is fixing.
- Classification against a staleness window is what stops false positives muting the job.
- Missing, divergent and orphan are three different signals; an orphan usually means your ownership map is wrong.
- A bounded, window-scoped mode is what makes the reconcile step of an incident executable.
The chain, answered
Every field here is required, which is why no lesson in this domain can recommend a design without naming what an operator sees when it fails, what survives the partition, what repairs it afterwards, and the simpler thing to consider first.
- • Enumerate the entities to compare, cheaply — by checksum or count over key ranges, refining only where they differ.
- • Read the value from the source of truth and from the derived store.
- • Exclude differences inside the acceptable staleness window as expected lag.
- • Classify remaining differences as missing, divergent or orphan.
- • Repair idempotently from the source, rate-limited so the repair cannot itself cause an incident.
- • Emit the delta size and the oldest drift age as metrics, and alert on a step change rather than on any non-zero value.
- • The comparison is run against a moving system and reports differences that were merely in flight.
- • Repair is unbounded and saturates the store it is repairing.
- • The job repairs the wrong direction because authority was ambiguous.
- • Orphans are auto-deleted and destroy data that existed only in the derived store.
- • The job runs but nobody sees the output, so a systematic bug is repaired nightly for months.
- • Two instances of the job run concurrently and fight over the same records.
- • Silent concealment: the operator sees a healthy system for months, then learns a projection has been wrong the whole time and the nightly job has been repairing it without ever reporting a number.
- • Muted job: the operator finds the reconciliation alert disabled six months ago because it fired constantly — it was comparing without a staleness window and every recent write looked like drift.
- • Repair-induced incident: the operator sees database latency spike at 02:00 nightly, traced to a reconciliation job rewriting two million records with no rate limit.
- • Wrong-direction repair: the operator sees correct values in the source being overwritten with stale ones, because the job was configured with the derived store as authoritative.
- • Orphan deletion as data loss: the operator sees three years of operator annotations deleted by a cleanup pass, because the derived store held fields the source never had.
- • Growing delta ignored: the operator finds the delta metric has risen steadily for eight weeks with no threshold configured, and the underlying cause is a projection bug shipped two months ago.
- • Reconciliation needs both stores readable at once, which is a coordination requirement that can be unavailable exactly when drift is largest.
- • It requires no coordination between the stores themselves — it observes and repairs, it does not negotiate — which is why it works across boundaries where a transaction cannot.
- • Repairing requires unambiguous authority; without it the job has no defined direction and must not run.
- • Concurrent runs must be coordinated by a lock or a lease, or two instances will fight over the same records and produce churn.
- • While reconciliation is not running, drift accumulates silently and unboundedly — the divergence window is exactly the cycle time.
- • If the source is unavailable, reconciliation must stop rather than repair from the derived side; repairing in the wrong direction is worse than not repairing.
- • A partial run leaves some ranges reconciled and others not, which is safe provided progress is recorded so the next run resumes rather than restarts.
- • The delta metric remains meaningful even when repair is disabled, so detection can continue when repair cannot — a useful mode during incidents.
- • Detect: the delta metric is itself the detection mechanism for the whole derivation pipeline; alert on a step change and on oldest-drift age.
- • Contain: disable repair but keep comparison running when the source is degraded, so you keep visibility without risking wrong-direction writes.
- • Recover: repair in bounded batches with a rate limit, oldest drift first.
- • Reconcile: for divergences that indicate a projection bug, fix the projection and replay rather than repairing record by record — repair treats symptoms.
- • Verify: re-run the comparison over the same range and confirm a zero delta, and confirm the delta metric has returned to its baseline rather than to zero-because-nothing-ran.
- • Delta size per derived store, as a standing gauge — the single most valuable signal about the health of a derivation.
- • Oldest drift age, which distinguishes a fresh burst from a long-standing systematic problem.
- • Breakdown by classification: missing, divergent, orphan. Each trend means something different.
- • Reconciliation run health: last successful run, duration, coverage, and records repaired.
- • Repair rate against the store’s capacity, so a repair pass is visibly bounded rather than hopefully bounded.
- • Any system with derived state — caches, indexes, read models, partner copies — which is nearly all of them.
- • Cross-boundary duplication of data, where no transaction spans the two stores and nothing else will notice divergence.
- • Financial, inventory and other domains where a silent discrepancy has direct external cost and will eventually be found by someone outside engineering.
- • As the executable fourth step of incident response, bounded to the incident window.
- • Systems with no derived state, where there is nothing to compare.
- • When it becomes a substitute for fixing the pipeline: nightly repair of the same divergence is a bug report, not a solution.
- • When comparison load is significant relative to the stores’ capacity and it has not been run off replicas or rate-limited.
- • When authority is ambiguous, in which case the job can make things worse and should not run until ownership is settled.
- • Rebuild the derived store from scratch on a schedule: simpler than comparison and correct by construction, and viable whenever the rebuild is cheap enough — it also exercises the rebuild path, which is a second benefit.
- • A transactional outbox, which removes the commit-versus-publish partial failure and eliminates one of the six drift sources — narrowing the problem without closing it.
- • Checksums or version counters propagated with the data, so a derived store can detect its own staleness for a key rather than needing a full comparison.
- • Read-repair: verify against the source on read for hot keys and repair inline. Cheap, and it only ever reaches the records people look at — the ones that drift unnoticed are the ones nobody reads.
Reconciliation is a component, with an owner and a schedule
# a gauge, not a log line
derived_store_delta_rows{store="search-index"} 90
reconcile_last_success_timestamp <now>
reconcile_rows_compared 10000000
# alert on a STEP CHANGE in the delta, not on delta > 0.
# a small steady delta is the staleness window; a step is a broken pipeline.What people believe, and what is true
If our pipeline were correct we would not need reconciliation.
The pipeline is a distributed system with at-least-once delivery. Messages will be lost, duplicated and reordered regardless of implementation quality. Reconciliation is what makes that operable.
We will write a script if we ever see a discrepancy.
You will not see it — that is the defining property. And a script written during an incident, by a tired engineer, against recovering stores, with no rate limit or staleness window, is the tool most likely to widen the incident.
The job repairs everything automatically, so we are fine.
Silent repair conceals the bug it is repairing. Without alerting on the delta, a systematic projection error can be repaired nightly for a year while everyone believes the pipeline is healthy.
Records in the derived store that are not in the source are orphans and should be deleted.
Sometimes they are data that exists only there, which means the store is not fully derived. Surface orphans; auto-delete only after the reconstruction test says the store is genuinely derived.
Reconciliation is a batch job, so it does not affect production.
It reads both stores and rewrites records. Without replicas and a rate limit, it is a substantial and badly timed load — the classic 02:00 latency spike.
Go deeper
Only the levels this lesson can honestly fill — a missing level is a claim nobody had.
Overview
Derived state drifts. Compare the source against each derivation, classify the differences, repair from the source, and alert on the size of the delta. It is a component with an owner, not a script.
Practical
Compare cheaply with range checksums first. Exclude differences inside the acceptable staleness window or the alert gets muted. Repair idempotently and rate-limited. Emit the delta as a gauge and alert on a step change. Add a window-bounded mode so it is usable during an incident.
Advanced
Reconciliation is anti-entropy applied across a service boundary instead of between replicas, and it inherits both the technique and the trade-off: comparison cost against detection latency. The same efficiency ideas transfer — hierarchical checksums to avoid full scans, incremental sweeps over recent windows, read-repair for hot keys. What does not transfer is the assumption that both sides are equal peers: across a boundary one side is authoritative, which makes the repair direction trivial and makes ambiguity about authority fatal to the whole mechanism.
Apply it
- 🔧 Pick one derived store and write the comparison — report only, no repair. Record the delta for a week before deciding what to alert on.
- 🔧 Add a window-bounded mode to an existing reconciliation job so it can be scoped to an incident window and run in report-first mode.
- ⚡ A nightly reconciliation has been repairing about 400 divergent records every night for six months, silently. What are the possible causes, and what is the first thing you change?
- 💬 Your search index and database disagree. What component finds that, and what does it do?
- 💬 Why is alerting on the delta more important than repairing it?
- 💬 Your reconciliation job reports thousands of discrepancies every run. What is probably wrong?
- 💬 The derived store has a record the source does not. What are the two explanations, and why does the difference matter?