State Projection over an ADS-B Stream: One Pattern, Seven Names
Table of Contents
- 1. The problem, stated once
- 2. A concrete instance: two receivers, one aircraft
- 3. The importance of language
- 4. What the projection cannot see
- 5. The defects, named in every language
- 6. Measured on our own two receivers
- 7. Rebuild: the fold as code
- 8. Clojure / Datomic: the reading where the model is ADS-B
- 9. The refutation condition
- 10. Precursors: the pattern is older than the words
- 11. Why the language matters
- 12. Sources
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.
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.
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
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
piheard the aircraft seven seconds beforehydra2. 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
picontributed 1,608 messages andhydra286, or thathydra2dropped 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:
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
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:
pinever 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.
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:
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 disagree — pi
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.
)
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) → valuefails; the correct key becomes(hex, field, ts)and the projection must aggregate (MIN, MAX, COUNT), each a different projection. The flag/current-state form isCOUNT > 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:
piandhydra2, 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.