Problem
A radar does not hand you aircraft. It hands you blips: one azimuth, range and altitude reading per beam sweep, with nothing attached that says which object produced it. Two consecutive sweeps give you two unrelated sets of points. Something has to decide which blip in sweep N+1 is the same object as which blip in sweep N.
The timing makes this harder than it sounds. The beam rotates at 36°/s, so a full sweep takes 10 seconds. In those 10 seconds a target closing head-on at 550 knots, against an ownship doing 450 knots, has moved up to 5.1 km. The naive answer — match each blip to the nearest track — breaks down as soon as two contacts pass within that distance of each other.
Approach
The search radius comes from the geometry, not from tuning. Maximum relative speed is 550 + 450 = 1000 knots, or 514.4 m/s. Over one 10-second sweep that is 5144 m. With a 1.4 safety margin the correlation gate lands at 7202 m. Any blip further than that from a track’s predicted position cannot belong to it. The number is derived rather than guessed, which means it moves correctly if the scan rate or the speed envelope changes.

Prediction and distance. Each track’s expected position is extrapolated with a constant-velocity model built from its last two measurements. Blips arrive as ownship-relative polar coordinates and are converted to geodetic lat/lon, so the track-to-blip distance is a Haversine calculation on the sphere rather than a flat-plane approximation.
Matching. Candidate pairs inside the gate are sorted by distance and consumed greedily: each track and each blip can be used at most once. It is locally optimal, not globally — more on that below.
Lifecycle. A track starts Tentative on its first unmatched blip and only becomes Confirmed on a second consistent measurement. It goes Coasting after 30 seconds without a measurement, and is Dropped after 50. Only Confirmed and Coasting tracks are published. Tentative ones stay internal, so a single noise return never paints a contact on the operator’s screen.

Architecture. The service is organised hexagonally. The domain core — blip, track, correlation engine, coordinate transform, platform classifier, alert — knows nothing about gRPC or protobuf. The application layer sits around it, and everything protocol-shaped lives in the outermost ring. The reason is testing: correlation is the part that most needs to be exercised in isolation, and it is exactly the part that should not require a running server to test.

Unit handling follows the same boundary discipline. The radar speaks degrees, nautical miles, feet and knots; the domain speaks metres and metres per second. Conversion happens once, at the edge.
What did not work
The first version assumed blips carried a stable identity across sweeps. If each blip arrived with an ID that persisted, correlation would collapse into bookkeeping: register a hit when the ID reappears, register a miss when it does not, and the track is just a running tally against that key.
That assumption was wrong. Blip IDs are assigned per sweep and do not carry over — the same physical aircraft comes back under a different ID every ten seconds. The hit/miss counters were keyed on something that changed underneath them, so tracks fell apart on every rotation.
The fix was to drop identity as an input entirely and treat every sweep as an anonymous set of measurements. What survives across sweeps is the track, not the blip: each track ages on its own clock, gains confidence when a geometrically plausible measurement lands inside its gate, and decays toward Coasting and Dropped when none does. Correlation became a matching problem instead of a lookup, and the lifecycle state machine exists because that is what has to carry the identity the data does not provide.
Result
108 GoogleTest cases covering correlation, lifecycle transitions, coordinate conversion and the multithreaded service flows. The core processing loop runs on a 500 ms budget, fed by a thread-safe blip queue, with structured JSONL logging for after-the-fact inspection.
Two limits are worth naming. Greedy matching is locally optimal: in dense traffic a blip claimed by a slightly nearer track can starve a better overall assignment, which a global method like the Hungarian algorithm would avoid at higher cost. And constant-velocity prediction degrades during manoeuvres — the natural next step is a proper tracking filter.
