Reconciliation Strategy Decisions › Batch Reconciliation vs Streaming Reconciliation

Batch Reconciliation vs Streaming Reconciliation

This page answers the cadence question in depth: should you reconcile two systems in scheduled batches over frozen snapshots, or continuously as changes flow? It sits under the reconciliation strategy decisions reference and goes past the quick decision framework to the real trade-offs — freshness, cost, correctness under motion, and operational load — that determine which cadence fits a given system, and when the right answer is both.

Problem Framing

You are cutting over an orders database and must prove the target never diverges from the source. A nightly batch reconciliation is simple to build and cheap to run, but its verdict is up to a day stale — useless the moment a stakeholder asks “are they in sync right now?” A streaming reconciler answers that question continuously but is a long-lived service with watermark alignment, checkpointing, and on-call weight. The naive framing treats these as competitors; the mature framing treats them as complementary. Batch excels at exhaustive, point-in-time proof over a frozen snapshot; streaming excels at bounded-latency detection of drift as it happens. Most serious migrations run streaming for live detection and a periodic batch as an exhaustive backstop that catches anything the stream’s tolerances let through.

Implementation

The choice reduces to a small set of measured inputs. The helper below scores cadence and flags when a hybrid — streaming plus a batch backstop — is warranted, so the decision is reproducible rather than a matter of taste.

python
import logging
from dataclasses import dataclass

logging.basicConfig(
    format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", level=logging.INFO
)
logger = logging.getLogger("recon.arch.cadence")


@dataclass(frozen=True)
class CadenceInputs:
    freshness_seconds: int          # max acceptable staleness of a verdict
    change_rate_per_sec: int        # source mutation rate
    has_change_stream: bool
    is_regulated_cutover: bool      # a live migration under audit


def recommend_cadence(i: CadenceInputs) -> dict:
    """Return a cadence and whether a batch backstop should accompany it."""
    if i.freshness_seconds <= 60 and i.has_change_stream:
        primary = "streaming"
    elif i.freshness_seconds <= 60:
        primary = "micro_batch"      # sub-minute need, no stream available
    else:
        primary = "batch"
    # A live regulated cutover always deserves an exhaustive periodic backstop.
    backstop = primary != "batch" and (i.is_regulated_cutover or i.change_rate_per_sec > 1000)
    decision = {"primary": primary, "batch_backstop": backstop}
    logger.info("cadence recommendation: %s", decision)
    return decision

Cadence Trade-Off

The two cadences differ on every axis that matters operationally.

Axis Batch Streaming Compliance / regulatory
Verdict freshness Hours to a day Seconds Streaming meets fixed-window regulated reconciliation; batch may not.
Correctness model Exhaustive over a frozen snapshot Bounded by watermark and tolerance Batch gives a clean point-in-time proof; streaming needs a lateness policy.
Cost profile Low; runs then stops High steady-state; always on Batch is cheaper to justify; streaming’s cost is the price of freshness.
Operational load Low; a scheduled job High; a long-lived service with on-call Streaming adds a runtime audit surface (checkpoints, dead-letters).
Failure blast radius A whole run reruns One partition degrades, others continue Streaming isolates failure; batch reruns are simpler to reason about.

Key Implementation Notes

  • Freshness, not volume, is the binding input. Volume decides the engine; freshness decides the cadence. A petabyte reconciled nightly is still batch; a kilobyte that must agree within seconds is streaming.
  • Streaming’s correctness is conditional; batch’s is exhaustive. A streaming verdict is “equal up to watermark W within tolerance”; a batch verdict is “equal over this exact snapshot.” That is why a regulated cutover pairs streaming detection with a batch proof — the structural diffing backstop catches what the stream’s watermark alignment and tolerances permit.
  • Micro-batch is the honest middle. When freshness is tight but no change stream exists, small frequent batches approximate streaming without a full stream processor, at a fraction of the operational cost.
  • The backstop is not optional under audit. For a live regulated migration, run the periodic exhaustive batch even alongside streaming; it is the artifact that proves completeness when a streaming tolerance is later questioned.

Verification

Assert the framework recommends streaming with a backstop for a regulated live cutover and plain batch for a high-freshness-tolerant workload.

python
live = CadenceInputs(freshness_seconds=30, change_rate_per_sec=5000, has_change_stream=True, is_regulated_cutover=True)
rec = recommend_cadence(live)
assert rec == {"primary": "streaming", "batch_backstop": True}

nightly = CadenceInputs(freshness_seconds=86_400, change_rate_per_sec=10, has_change_stream=False, is_regulated_cutover=False)
assert recommend_cadence(nightly)["primary"] == "batch"
logger.info("cadence decision verified")

Operational Considerations

Run the batch backstop on a cadence tied to your audit window — daily for most regulated cutovers — and reconcile its exhaustive verdict against the streaming reconciler’s running state so a divergence between the two immediately flags a streaming tolerance that is too loose. Budget for streaming’s steady-state cost explicitly, since an always-on reconciler is a line item the batch model never had, and size the batch engine per the reconciliation strategy decisions framework. Emit a single verdict_freshness_seconds metric across both paths so stakeholders see the actual staleness of the last proof, not the theoretical cadence, and persist both the streaming and batch verdicts to the same audit store so a reviewer can see they agree.