State Projection over an ADS-B Stream: One Pattern, Seven Names

Table of Contents

1. The problem, stated once

There is one pattern under a dozen names. You have an append-only stream of events. You fold it into a materialized state view: one row per key, holding current state. The fold is cheap, the view is convenient, and the view forgets three things — the order the events arrived in, how many times each arrived, and where each came from.

Every community that has met this pattern gave it its own vocabulary. In Kafka it is an aggregate producing a KTable; in cloud messaging it is a consumer maintaining a table; in relational algebra it is a view over a base relation; in event sourcing it is a projector building a read model; in Kleppmann's derived data it is a materialized view; in event-driven microservices it is a stateful consumer materializing an entity; in order theory it is a monotone map from traces to down-sets. Same fold, seven words.

This report uses ADS-B as the worked instance, because ADS-B is a pattern you can point an antenna at. Two software-defined radios, a few miles apart, each hear the same aircraft. Merging their two streams into one track is a fold — and the merge makes the three forgotten things physical: two receivers with independent clocks, so order is a lie; the same aircraft heard twice, so multiplicity is real; and a track that cannot tell you which radio heard what, so provenance is gone.

The claim is not that ADS-B is special. The claim is the opposite: the importance is the language. Once you can name the merge defect in all seven vocabularies, you can talk to the streaming team, the database team, and the category theorist about the same bug without translating twice.

2. A concrete instance: two receivers, one aircraft

On 2026-09-07 a Lufthansa Airbus A380, registration D-AIMC, ICAO hex 3C65A3, flew LH424 Munich→Boston and landed at Logan at 17:30:45 EDT. It was heard on final descent by two of our RTL-SDR receivers in East Boston (42.3693, -71.0412): pi and hydra2, both feeding dump1090.

flow.png

The three artifacts are exactly the three artifacts of the general pattern:

artifact ADS-B thing the pattern
the stream squitters (DF17/18), per receiver, timed item_trace
the fold the tracker: merge by hex, decode CPR fold()
the projection the track / current-state row item_state

Here is the real head of both streams, and the two merged by receive time. Note that pi is already at 9,275 ft and settled before hydra2 hears a single squitter, and that once both are hearing it the messages interleave at sub-second spacing:

--- pi ---                         --- hydra2 ---
21:17:08.006  pi     MSG4          21:17:15.230  hydra2 MSG8
21:17:08.169  pi     MSG3 9275     21:17:17.720  hydra2 MSG4
21:17:08.169  pi     MSG7 9275     21:17:18.704  hydra2 MSG5 9150
21:17:08.659  pi     MSG7 9275     21:17:18.966  hydra2 MSG3 9150
21:17:13.747  pi     MSG7 9225     21:17:19.818  hydra2 MSG8
...                                ...

--- merged by receive time (which receiver, interleaved) ---
21:17:15.161  pi     MSG3 9200
21:17:15.230  hydra2 MSG8            <- 69 ms after a pi frame, different radio
21:17:17.720  hydra2 MSG4
21:17:18.966  hydra2 MSG3 9150

Capture totals: pi heard 3C65A3 across 222 distinct seconds (1,608 messages, acquired at 9 nm / 9,275 ft, held to 25 ft at touchdown); hydra2 across 32 seconds (86 messages, 8 nm / 8,925 ft, lost at 7,250 ft). The two overlap on 29 seconds. Everything below is about what the projection can and cannot say about those 29 seconds.

3. The importance of language

This is the center of the report. The same three artifacts, the same defects, and the same invariants have a name in every vocabulary. The value is not any one column; it is the row. When a merge produces an out-of-order track, the streaming engineer, the DBA, and the theorist are all looking at the same fact — but they will only discover that if someone has the table.

rosetta.png

3.1. The three artifacts, in seven languages

artifact KS (Kafka) MQ (cloud msg) RA (relational) ES (event sourcing) DD (derived data) OT (order theory) ADS-B
the stream topic keyed by hex; offset is order FIFO queue, group=hex relation T(hex,field,ts,rx) event log system of record path in transition graph squitter stream (DF17), per rx
the fold Streams aggregate + state store consumer view definition projector derivation fn functor P : Trace → Down(S) tracker: merge hex + CPR pair
the projection KTable / compacted topic consumer's table pivot / view of T read model materialized view subset of S, valid iff down-set the track / current-state row

3.2. One defect, seven names

The single defect that ADS-B makes physical is out-of-order in a merged stream. Its names:

vocabulary name for "later squitter sorted first after merge"
KS out-of-order across partitions; event time ≠ processing time
MQ cross-group ordering not guaranteed
RA a self-join on T finds a.ts > b.ts with seq(a) < seq(b)
ES late-arriving event; the read model was stale when read
DD timeliness failure, not integrity; straggler
EM out-of-order in the entity stream
OT P forgets order; it is not faithful
ADS-B two receivers with skewed clocks; a total order was assembled from more than one source

The ADS-B row is the one you can prove with an antenna. A single receiver delivers squitters in the order it heard them. If a merged feed is out-of-order, more than one receiver contributed and their clocks disagree — which is exactly what our 69 ms interleave between pi and hydra2 at 21:17:15 shows. The change-data-capture community says the same thing a different way: a write-ahead log is totally ordered by LSN, so an out-of-order trace was assembled from more than one WAL.

4. What the projection cannot see

loss.png

The projection — the aircraft's current lat/lon/alt/callsign — is a COUNT(*) > 0 over the merged stream, keyed by hex. It answers "where is 3C65A3 now" perfectly and answers nothing about the 29-second overlap:

  • Order is gone. The row does not know pi heard the aircraft seven seconds before hydra2. Only the log, ordered by receive time per receiver, knows that.
  • Multiplicity is gone. In the overlap the same position was heard twice; the projection keeps one value per field. This is compaction: one value per key, multiplicity discarded.
  • Provenance is gone. The row cannot say pi contributed 1,608 messages and hydra2 86, or that hydra2 dropped the aircraft at 7,250 ft. The track is receiver-agnostic by construction; that is a feature for display and a defect for diagnosis.

This is the general theorem in physical form: a projection is a function of the stream that forgets everything the key does not carry. If you need order, multiplicity, or provenance, you need the log — the raw per-receiver squitters on /mnt/usb/adsb/raw/{pi,hydra2}-latest.csv. The track is where you look to fly; the log is where you look to explain.

5. The defects, named in every language

The message distribution on pi (n = 1,608) is itself an argument for keying on identity, not on the field you happen to want:

MSG type n %
air-to-air (DF0/16) 881 55
airborne position (3) 183 11
velocity (4) 181 11
surveillance alt (5) 179 11
all-call reply (8) 166 10
identity/callsign (1) 18 1

The callsign (DLH8P inbound — note it is not the flight number LH424) rides only the 1 %-frequency identity squitter, and it arrives late. So the tracker keys on the ICAO hex 3C65A3, present on every frame, exactly as event sourcing keys the projector on an aggregate id and relational algebra keys the view on a primary key. Keying on the callsign would be keying on the rarest, latest field — a design error with a name in every column.

The classic projection defects, and where each lives in ADS-B:

defect ADS-B cause RA ES DD OT
duplicate key (I5) same squitter heard by pi and hydra2 PK violation fold ran twice non-idempotent P applied twice
out-of-order (I6) receiver clock skew (the 69 ms interleave) ts self-join late event timeliness P not faithful
unknown symbol unhandled DF / type code FK violation bad event log admitted bad not a down-set
domain violation CRC failure decoded to an in-range wrong value domain serializer bug corruption outside 2^S
arity violation truncated / extended frame arity schema drift schema drift encoding error

Two of these are worth dwelling on because ADS-B states them more sharply than the abstract pattern does.

Duplicate is not an error here; it is the mechanism. The whole reason to run two receivers is that both hear the aircraft, so the merged stream is supposed to contain the same position twice. Deduplication — by hex plus frame content, ignoring receiver and receive-time — is the fold's job, and it is idempotent by construction: replay the log, get the same track. Every vocabulary calls the fix idempotence (ES: replay; DD: reprocessing; MQ: redrive with dedup; RA: the view is a function of T; OT: P is a function).

The CRC-decoded-to-a-valid-wrong-value case is the one the domain check cannot catch. A frame that fails CRC but decodes to an in-range altitude is ITM-05 generalized: a value inside the domain that is still wrong. No schema-domain constraint sees it; only cross-frame agreement (two receivers, or an even/odd position pair that fails to close) exposes it. This is the Baudot shift-state failure — one bit of state, lost, and every subsequent symbol decodes to a valid but wrong value.

5.1. The one dependency: CPR pairing as a down-set

ADS-B position is not in a single frame. A global position needs an even and an odd Compact Position Reporting frame within about ten seconds; a lone frame gives only a local or ambiguous fix. So the position projection has a genuine predecessor rule — the order-theory invariant I1, "state set only if its predecessor is set" — realized in radio:

cpr.png

A projection that emits a position from a lone frame is emitting a state whose predecessor is unset: not a down-set, in the order-theory reading; a foreign key with no referent, in the relational reading; a read model reporting a state the aggregate never reached, in event sourcing. Same defect, and here the "predecessor" is a second radio frame you either have or do not.

6. Measured on our own two receivers

Everything above is a claim about what a projection forgets. Here it is, counted — over months, then one hour — from our two receivers (pi and hydra2, KBOS arrivals). (Charts rendered from the raw logs in Clojure with cljplot; the loaders and specs are in the flight-tracking repo.)

6.1. Over months: the day a receiver went dark

adsb-coverage.png

Through July both receivers ran clean — ~23.3 data-hours per day each. Then on 2026-08-07 hydra2 went dark and stayed dark for ~30 days (766 lost receiver-hours), while pi carried on at a full 24/24. Two facts from the same span sharpen the point:

  • pi never lost an hour in all of 2026, and there was never a single hour where both receivers were empty — every loss was one-sided.
  • Through that month-long outage the track looked identical. A consumer of the projection could not tell that half the sensing had failed; provenance quietly halved and the current-state view never blinked. That is the whole argument in one operational event: the projection is exactly the wrong place to notice you have lost a receiver, and the log is the only place you can.

6.2. One hour, in detail

From 21:00–22:00Z on 2026-09-07 (pi + hydra2, the D-AIMC arrival hour): 293,639 messages, every row a clean 24-field frame — no arity or domain defects that hour.

Missed data (coverage). pi heard 140 aircraft, hydra2 96. Every hex hydra2 heard, pi also heard (hydra2 ⊂ pi): 44 aircraft were on pi only — the second receiver simply missed them. Even on aircraft both heard, coverage differs: on the D-AIMC arrival pi held the target from 9 nm to 25 ft at touchdown while hydra2 dropped it at 7,250 ft. The track shows one aircraft; which receiver could see it, and how far down, is in the log.

Overlap (multiplicity). 95 aircraft were heard by both receivers in the same second — the top one (AC1B60) for 286 overlapping seconds. Those are the duplicate frames the fold must collapse; more receivers, more multiplicity, and it is all invisible in the track.

Idempotency (exact duplicates). Within a single receiver the same frame (hex, millisecond, type, content) recurred 55,878 times on pi and 5,244 on hydra2 — about a fifth of the stream. The projection is unbothered: its step is set insertion, idempotent by construction. Replay the hour, get the same track.

Incorrect sequencing (clock skew, I6). For frames heard by both receivers at the same altitude in the same second — 2,866 such pairs — the two receive-times disagree: hydra2 − pi offset median −56 ms, 5th–95th percentile −599..+379 ms, extremes near ±1 s.

skew-histogram.png

That is the skew, measured. And the subtle part: merging the two position streams by receive time produced zero altitude-order inversions against each aircraft's climb or descent. The reorder is real in receive time yet sub-threshold in the projection, because altitude moves slowly relative to the frame rate — so the only way to see the skew is to compare the two receivers' timestamps for the same frame. That is exactly the skew query, and exactly what the track has thrown away.

Coverage in space. Twenty days of arrivals, first acquisition (▲) and last contact (▼) for the LH A380 into Boston:

inbound-acquisition-map.png

Our receivers grab each arrival on final descent at a median ~10 nm; where a track begins and ends — provenance, again — is in the log, not the track.

7. Rebuild: the fold as code

The tracker is a fold and nothing more. It keys on the hex, merges the two receivers, and — crucially — answers order questions only from the log, never from the projection.

import csv, glob, gzip
from collections import OrderedDict

# SBS columns (dump1090): recv_ts, receiver, MSG, type, ..., hex(6), ..., alt(13), ..., lat(16), lon(17)
def stream(receiver):
    for f in sorted(glob.glob(f"/mnt/usb/adsb/raw/{receiver}-latest.csv/2026/09/07/*.csv.gz")):
        with gzip.open(f, "rt", errors="ignore") as fh:
            for ln in fh:
                c = ln.rstrip("\n").split(",")
                if len(c) > 17 and c[2] == "MSG" and c[6] == "3C65A3":
                    yield {"ts": c[0], "rx": receiver, "type": c[3],
                           "alt": c[13], "lat": c[16], "lon": c[17]}

def fold(*receivers):
    """projection: one row per hex, last-write-wins per field. Merges receivers,
    dedups by (field-value), keeps NO order or provenance -- by design."""
    rows = sorted((r for rx in receivers for r in stream(rx)), key=lambda r: r["ts"])
    state = OrderedDict()
    prov  = {}                       # provenance kept OUTSIDE the projection
    for r in rows:
        s = state.setdefault("3C65A3", {})
        if r["alt"]: s["alt"] = r["alt"]
        if r["lat"] and r["lon"]: s["lat"], s["lon"] = r["lat"], r["lon"]
        prov.setdefault("3C65A3", {}).setdefault(r["rx"], 0)
        prov["3C65A3"][r["rx"]] += 1
    return state, prov, rows

def check_order(rows):
    """I6 -- only expressible on the LOG. Out-of-order after merge => >1 clock."""
    out, last = [], {}
    for r in rows:
        # per-receiver the stream is monotone; across receivers the merge is not
        if r["rx"] in last and r["ts"] < last[r["rx"]]:
            out.append((r["ts"], r["rx"], "non-monotone within receiver (impossible)"))
        last[r["rx"]] = r["ts"]
    # cross-receiver interleave is expected; report the tightest gap as evidence
    return out

if __name__ == "__main__":
    state, prov, rows = fold("pi", "hydra2")
    print("projection :", state["3C65A3"])          # what you fly on
    print("provenance :", prov["3C65A3"])            # pi=1608 hydra2=86 -- NOT in the projection
    print("merged msgs:", len(rows))

The relational reading makes the schema gate literal: constraints on the base relation are checked before any view is evaluated, and the order invariant is a self-join that the view cannot express.

CREATE TABLE squitter(              -- the log: one row per received frame
  hex   TEXT NOT NULL,
  rx    TEXT NOT NULL,              -- provenance lives here, not in the view
  ts    TEXT NOT NULL,
  type  TEXT NOT NULL,
  alt   INT, lat REAL, lon REAL,
  PRIMARY KEY (hex, rx, ts, type)   -- same frame, two receivers => two rows, kept
);

-- the projection: one row per aircraft, provenance and order aggregated away
CREATE VIEW track AS
SELECT hex,
       MAX(alt) FILTER (WHERE type='3') AS alt,
       COUNT(*)                          AS n_frames   -- multiplicity collapsed to a count
FROM squitter GROUP BY hex;

-- I6: expressible ONLY on the log, never on the view
CREATE VIEW v_order AS
SELECT a.hex, a.rx, b.rx, 'I6 out-of-order' AS inv
FROM squitter a JOIN squitter b USING(hex)
WHERE a.rx <> b.rx AND a.ts > b.ts AND a.type = b.type;  -- two clocks disagree

Both rebuilds agree on the contract: the projection is a function of the log; provenance and order are not recoverable from the projection and must be asked of the log; and replaying the log yields the same track (idempotence).

8. Clojure / Datomic: the reading where the model is ADS-B

Six of the seven vocabularies describe a projection that sits beside a log. The seventh — Datomic — is the one whose data model already is the ADS-B model: the log is the truth, indexes are derived, and every database is a value you can query. Because our capture stack is Clojure (babashka), this is not a metaphor — the squitter log is an append-only set of datoms, the track is a query against a db value, and the two things ADS-B loses (order and provenance) are exactly the two Datomic keeps in history and :db/txInstant.

Three ADS-B facts that the other readings only gesture at fall out directly.

1. Bitemporality is the multi-receiver clock skew. A squitter has two times: when the aircraft transmitted it (its valid time, which each receiver stamps on receipt) and when it was ingested (its transaction time). One receiver and the two move together; two receivers and the valid times disagreepi stamped the 9,200 ft position at 21:17:15.161, hydra2 its copy at 21:17:15.230, 69 ms later. That disagreement is the skew, and Datomic is the only vocabulary here that carries both times first-class, so "which receiver's clock, and how far off" is a query, not a forensic reconstruction.

2. as-of is track replay; history is the order the track dropped. The current track keeps one position per aircraft. (d/as-of db t) gives the track as it stood at ingest time t; (d/history db) keeps every receiver's report, including the ones the track superseded — which is where the I6 out-of-order lives.

3. Retraction, not edit, is the fix. A CRC-passed-but-wrong altitude is corrected by retracting the bad datom and asserting a good one in a new transaction; as-of before the fix still shows the error. Never edit history.

{:deps {org.clojure/clojure {:mvn/version "1.12.0"}
        com.datomic/local   {:mvn/version "1.0.285"}}}
;; datascript {:mvn/version "1.7.4"} is the in-memory alternative — same
;; log-as-truth model, no Datomic dependency. Runs under clj on the JVM
;; (verified, FreeBSD/OpenJDK 21); it does NOT load in babashka out of the box.
;; Both this and Datomic Local are JVM libraries: available anywhere a JVM is,
;; FreeBSD included — not a native package.
(ns adsb.track
  (:require [datomic.client.api :as d]))

;; A squitter is a datom-set. :squitter/recv-ts is VALID time (the receiver's
;; clock); :db/txInstant (free, on every datom) is TRANSACTION time (ingest).
;; The unique tuple dedups a re-logged frame from ONE receiver (I5); the same
;; transmission heard by the OTHER receiver differs in :squitter/rx and is
;; kept — that is provenance, and it is the point of running two antennas.
(def schema
  [{:db/ident :rx/pi} {:db/ident :rx/hydra2}
   {:db/ident :squitter/hex     :db/valueType :db.type/string  :db/cardinality :db.cardinality/one}
   {:db/ident :squitter/rx      :db/valueType :db.type/ref     :db/cardinality :db.cardinality/one}
   {:db/ident :squitter/recv-ts :db/valueType :db.type/instant :db/cardinality :db.cardinality/one}
   {:db/ident :squitter/type    :db/valueType :db.type/long    :db/cardinality :db.cardinality/one}
   {:db/ident :squitter/alt     :db/valueType :db.type/long    :db/cardinality :db.cardinality/one}
   {:db/ident :squitter/uniq    :db/valueType :db.type/tuple
    :db/tupleAttrs [:squitter/hex :squitter/rx :squitter/recv-ts :squitter/type]
    :db/cardinality :db.cardinality/one :db/unique :db.unique/identity}])

;; real head of the D-AIMC (3C65A3) capture — one transaction per receiver
;; batch, so :db/txInstant records the ingest order (pi first, hydra2 later).
(def pi-batch
  [{:squitter/hex "3C65A3" :squitter/rx :rx/pi     :squitter/type 3
    :squitter/recv-ts #inst "2026-09-07T21:17:15.161Z" :squitter/alt 9200}])
(def hydra2-batch
  [{:squitter/hex "3C65A3" :squitter/rx :rx/hydra2 :squitter/type 3
    :squitter/recv-ts #inst "2026-09-07T21:17:18.966Z" :squitter/alt 9150}])

(defn track
  "the projection: current altitude for a hex, latest valid time across
   receivers. Dedups the two receivers at read; keeps neither which one nor
   the order they arrived."
  [db hex]
  (->> (d/q '[:find ?ts ?alt ?rx
              :in $ ?hex
              :where [?s :squitter/hex ?hex]
                     [?s :squitter/recv-ts ?ts] [?s :squitter/alt ?alt]
                     [?s :squitter/rx ?e] [?e :db/ident ?rx]]
            db hex)
       (sort-by first) last))

(defn skew
  "I6 / provenance: same aircraft, two receivers, valid-time gap. Expressible
   only over the log — the track has thrown :squitter/rx away."
  [db hex]
  (d/q '[:find ?rxa ?ta ?rxb ?tb
         :in $ ?hex
         :where [?a :squitter/hex ?hex] [?a :squitter/rx ?ea] [?ea :db/ident ?rxa] [?a :squitter/recv-ts ?ta]
                [?b :squitter/hex ?hex] [?b :squitter/rx ?eb] [?eb :db/ident ?rxb] [?b :squitter/recv-ts ?tb]
                [(not= ?rxa ?rxb)] [(< ?ta ?tb)]]
       db hex))

(comment
  ;; (def conn ...) ; Datomic Local, transact schema, then:
  ;; (d/transact conn {:tx-data pi-batch})       ; ingest pi
  ;; (d/transact conn {:tx-data hydra2-batch})   ; ingest hydra2 (later txInstant)

  (track (d/db conn) "3C65A3")
  ;; => [#inst "...18.966Z" 9150 :rx/hydra2]  latest valid time wins; pi's 9200 is
  ;;    gone from the track but NOT from the log.

  (skew (d/db conn) "3C65A3")
  ;; => #{[:rx/pi #inst"...15.161Z" :rx/hydra2 #inst"...18.966Z"]}  two clocks,
  ;;    ordered by valid time — the multi-receiver skew as a query.

  ;; as-of the txInstant between the two ingests: track shows pi's 9200. The
  ;; second receiver had not been ingested; the db value at that t proves it.

  ;; history keeps BOTH 9200 and 9150 with their :squitter/rx — the provenance
  ;; the current track dropped. (d/history (d/db conn)) is where I6 is legible.

  ;; fix a CRC-wrong altitude: (d/transact conn {:tx-data [[:db/retract eid :squitter/alt 40000]]})
  ;; then assert the corrected datom. (d/as-of db before-fix) still shows 40000.
  )

datomic.png

The skew query is the payoff. In SQL it was a self-join over T; in order theory it was "P is not faithful"; here it is one datalog clause over the log, and its existence — a non-empty result — is the proof that more than one receiver contributed and their clocks disagree. Everything the report has been saying about projection forgetting order and provenance, Datomic lets you not forget by keeping the log queryable as a value.

9. The refutation condition

State it as a theorem so it has an edge to attack.

Claim. The wide current-state projection is sound (its valid rows are exactly the down-sets of the state order) iff the transition graph is a DAG, and complete (a valid row determines current state) iff the graph is a chain.

For ADS-B the relevant break is re-entry. An aircraft can leave and re-enter a state — descend, level off, descend again; or a CPR position can wrap. When a state is re-enterable:

ES
a second position event overwrites the first in the read model; the model no longer reflects history.
DD
the wide form is a compacted view; compaction keeps one value per key and discards multiplicity, and re-entry is multiplicity.
MQ
the hex still orders delivery within one receiver, but the consumer's table has one slot per field and loses the earlier value.
RA
the functional dependency (hex, field) → value fails; the correct key becomes (hex, field, ts) and the projection must aggregate (MIN, MAX, COUNT), each a different projection. The flag/current-state form is COUNT > 0, the one aggregate that forgets multiplicity.
OT
a cycle makes the state order a preorder, not a poset; antisymmetry fails and the down-set characterisation has no content.

The moral is the same in every column: the current-state projection is the right tool for "where is it now" and the wrong tool for anything that happened more than once. Our D-AIMC descent is monotone enough that the track is faithful; a holding pattern would not be, and only the log would show it.

10. Precursors: the pattern is older than the words

None of the seven vocabularies invented this. The fold-into-a-projection is as old as writing things down as they happen and then summarising them. ADS-B is the modern terminus of a long line; four of its ancestors say something the software vocabularies do not.

precursor (era) stream fold projection what the projection loses
ship's logbook (centuries) hourly course, speed, wind dead reckoning plotted position the fix corrects the plot; the log keeps both
railway block signalling (1850s) bell codes; train register interlocking lever frame block indicator when the train passed, only that it did
stock ticker (1867) symbol–price ticks clerk posts the board quote board sequence; the board shows last price only
Baudot / teleprinter (1870s–) start bit, 5 data bits, stop bit frame + track letters/figures shift printed text the shift state — one bit, in no character
Hollerith tabulation (1890) punched cards tabulator counters the tally the cards; only totals survive
modem ARQ (1960s) framed packets, sequence numbers, CRC check CRC, ack/nak, reassemble the file retransmissions; the file has no memory
ADS-B (2000s) squitters: position, velocity, identity, DF-coded merge by ICAO addr + CPR odd/even pairs the track which receiver heard what, and the order

Four ancestors sharpen four of this report's points:

  • Railway interlocking is I1 (CPR pairing) as a physical lock: a signal cannot clear unless the lever frame proves the block empty. The invariant is in the mechanism, not checked afterward — the relational reading (constraints before views) with a lever you can touch. A signalman who altered the indicator without a bell code was dismissed: the anti-pattern of editing the projection instead of appending to the log, with a 170-year precedent.
  • Baudot shift state is why a projection can be silently wrong: one bit of state carried by the fold and present in no single symbol. Lose it and every later symbol decodes to a valid-but-wrong value — the CRC-passes-wrong-value case, which no domain check catches.
  • Dead reckoning vs. fix is integrity vs. timeliness, older by centuries: the log is integrity, the plot is a derivation, and the fix is a later event with better authority that corrects the projection without editing the log. That is how you fix a bad track: post a correcting observation, as a bookkeeper posts a reversing entry — never edit history.
  • ADS-B multi-receiver merge is the whole report in one line: out-of-order in a merged feed proves more than one receiver and disagreeing clocks. We measured it: pi and hydra2, 69 ms apart on the same aircraft.

11. Why the language matters

The engineering content of this report fits in one sentence: a projection is a fold of a log that forgets order, multiplicity, and provenance, so keep the log. Everyone who has built one of these knows that sentence. What they do not share is the words — and a bug report written in Kafka reaches the database team as noise, and a category-theory framing reaches the on-call engineer as nothing at all.

The two-receiver ADS-B capture is worth keeping precisely because it is concrete enough to anchor every vocabulary at once. "The merged track is out-of-order" is, simultaneously and provably: a processing-time-vs-event-time problem (Kafka), a cross-group ordering gap (SQS), a ts self-join hit (SQL), a late-arriving event (event sourcing), a timeliness rather than integrity failure (DDIA), an unfaithful functor (order theory), and two radios with skewed clocks (ADS-B). Being able to say all eight of those about one 69 ms interleave is the skill. The antenna is just what makes it real.

12. Sources

  • Sun, The 1090 MHz Riddle — ADS-B message types, DF codes, CPR odd/even position decoding.
  • Kleppmann & Riccomini, Designing Data-Intensive Applications 2e — derived data, integrity vs. timeliness, event time vs. processing time, log compaction, idempotence.
  • Hoffman, Real-World Event Sourcing — projector, projection, read model, aggregate vs. projection, "all data on events".
  • Bellemare, Building Event-Driven Microservices — event-carried state, single writer, entity vs. event streams, schema registry.
  • Kafka and Kafka Streams documentation — topic, partition, offset, consumer group, compaction, KTable, exactly-once semantics.
  • AWS SQS/SNS documentation — FIFO, MessageGroupId, deduplication, visibility timeout, DLQ.
  • Codd, the relational model — domain, key, functional and inclusion dependency, view.
  • Davey & Priestley, Introduction to Lattices and Order — down-sets, order ideals, monotone maps.