# When does an event at the batch-stream seam get counted twice or dropped? **Date of Record:** 2026 November 1 **Scope:** Applies to architectures that process the same data through a batch path and a streaming path and then combine the results, where each path decides boundary membership independently. Excludes lambda architectures that reconcile to a single source of truth. ## Core Distinction **Predicate.** A batch and a stream path partition the data if and only if their ranges meet at a boundary owned by exactly one side. If batch covers [t0, t1) and stream covers [t1, t2], an event at t1 is assigned once. If both are inclusive at t1, it is counted twice; if both are exclusive, it is dropped. **Failure.** Each path is correct for its own range. The combination double-counts or loses boundary events. Because the boundary is a single instant, the error is a step function of the event rate at that instant, not a gradual drift. **Asymmetry.** A dropped event is invisible: the combined total is simply lower, with no missing marker. A double-counted event is also invisible: the total is higher. Neither is an error at either path. ## What Was Observed **Signature.** Batch-plus-stream total differing from the source total by the event count at the seam instant, on days when the seam aligns with traffic. The delta is stable across runs and reproducible for a fixed seam. **Trace.** Batch job runs for [00:00, 24:00) inclusive of both ends. Stream starts at 24:00 inclusive. The event at exactly 24:00:00.000 appears in both the day's batch and the next day's stream, and is double-counted. If the batch is [00:00, 24:00) and the stream starts at 24:00 exclusive, the event is in neither. **Why monitoring stays green.** Both paths report the row counts they processed, and both are correct. The combined view is assembled by a consumer that has no independent count to compare against. ## Condition of Validity The combined result is exact only while at least one holds: 1. **Single seam owner.** The boundary instant is assigned to exactly one path by a shared rule, and the rule is enforced rather than assumed. 2. **Idempotent combine.** The combination deduplicates by event id, so double-counting is absorbed and gaps are the only remaining risk. 3. **Reconciliation.** The combined total is compared against a source-of-truth count on a schedule, with any delta alerted. Condition 2 resolves double counts but not gaps; 1 resolves both; 3 detects violations of either. ## Decision Boundary - Seam owned by one path, enforced: exact partition. - Both inclusive: double count at the seam. - Both exclusive: gap at the seam. - Idempotent combine: double counts removed, gaps remain. Acceptable only with a gap check. ## Operational Effect The seam is a coordination point between two systems with different owners and different scheduling, so it is rarely specified and never tested. Remediation is to make the boundary rule shared and explicit. The trap is to overlap the ranges slightly to avoid gaps, which converts a clean assignment problem into a guaranteed double count over the overlap. ```python from dataclasses import dataclass class SeamAssignmentError(ValueError): """Raised when batch and stream ranges do not assign the seam to exactly one side.""" @dataclass(frozen=True) class SeamGate: batch_end_inclusive: bool stream_start_inclusive: bool idempotent_combine: bool = False def verify(self) -> bool: if self.batch_end_inclusive and self.stream_start_inclusive: raise SeamAssignmentError("seam assigned to both paths: double count") if not self.batch_end_inclusive and not self.stream_start_inclusive: raise SeamAssignmentError("seam assigned to neither path: dropped events") return True def dispatch_combine_mode(gate: SeamGate) -> str: try: gate.verify() except SeamAssignmentError: return "dedupe-by-event-id" return "direct-sum" ```