All posts
Long-form notes on software, distributed systems, and the craft of building. Shipping one a week.
The Server Is a Sync Relay Now: Architecting Around Client-Owned State
At QCon London 2026 Kleppmann described local-first as the best of Google Sheets and the best of Git, and this year the sync engines shipped. From my reading of Electric, Zero, and LiveStore side by side, these are my notes on what changes when the client's copy becomes primary: where conflicts resolve, what the server still owns, and the merge-vs-refuse line that decides which apps must never be built this way.
Hybrid Logical Clocks: Making Last-Write-Wins Mean the Later Write
Wall-clock last-write-wins keeps the write from the faster clock, not the later event — and silently drops causally newer data under skew. These are my notes on rebuilding a Hybrid Logical Clock in Go: a 64-bit, monotonic, causal timestamp, why its counter stays bounded, and what it costs in CockroachDB-style uncertainty restarts.
Turning Repo Maintenance into Markdown: Keeping a Rust Codebase Alive with Agentic Workflows
Long-lived repositories drift: deprecated components linger, layers bleed, and tests miss the functions that actually break. In my own study I turned three recurring chores into scheduled markdown workflows the repo runs on itself, then wrote up what I learned about capping blast radius, pairing LLM checks with deterministic scans, and letting agents draft shapes while I write the substance.
What APISIX in the Trial Ring Actually Buys You: Notes on Its etcd-Backed Control Plane
Volume 34 of the Thoughtworks Technology Radar moved Apache APISIX into the Trial ring. I spent a week digging through the docs, source code, and a couple of bug reports to convince myself the etcd-backed dynamic-routing claim was real — and to weigh the operational cost it hides. These are my notes on the watch mechanism, the connection-scaling cliff at 263 long polls, and when I would and would not reach for APISIX in 2026.
Structured Concurrency Looks the Same in Four Runtimes — Until a Child Fails
I wrote the same fan-out four times — Java 25 StructuredTaskScope, Kotlin coroutineScope, Swift withThrowingTaskGroup, Python asyncio.TaskGroup — and the surface API is nearly interchangeable. The cancellation and exception-aggregation semantics are not. These are my notes on what diverges on the failure path and why only Python hands you every failure by default.
A Fitness Function Is Just a Test That Fails the Build When the Architecture Drifts
A fitness function is not a framework artifact — it is a build-failing test that encodes one architectural invariant. I encode a layering rule in about 60 lines of TypeScript using the compiler's own API, test the test against good, bad, and generated-code trees, then draw the line between an invariant worth gating and a metric gate that backfires under Goodhart's law.
Iceberg Schema Evolution: Drop-Then-Add Is Not a Rename
Apache Iceberg tracks every column by a unique numeric id, not by name. From my own digging into the spec and a small Kotlin program against a local catalog, the trap that bit me hardest is this: a drop followed by an add of the same column name is not a rename, and treating it as one quietly orphans your historical data.
Convergence Is a Property of Your Merge Function, Not the Network
I once watched an afternoon of offline edits vanish under a last-writer-wins sync, and the fix was not better networking — it was a better merge function. These are my notes on why CRDT replicas converge: a merge that is commutative, associative, and idempotent. I rebuild a minimal add-wins OR-Set in TypeScript, run it, and weigh what the guarantee costs in tombstones and memory.
Kotlin 2.4: The Three Changes That Moved My Hand on the Keyboard
Kotlin 2.4.0 shipped a long changelog, but only three features changed how I actually type: stable context parameters, explicit backing fields, and (still behind a flag) name-based destructuring. Here is my backend-engineer's cut, verified against the 2.4.0 compiler, plus the K1 removal I had to put on a calendar.
Catching a Retry Race with One Seed: Deterministic Simulation in Rust using turmoil
I had three flaky retry tests no one could reproduce on a laptop. I rewrote one in Rust on top of turmoil, Tokio's deterministic simulator, and a single 8-byte seed pinned the partition race byte-for-byte. These are my notes on what the seed actually controls, what leaks past it, and when deterministic simulation testing is worth the seam.