Update content & design (#75)
All checks were successful
Deploy to Pages / build (push) Successful in 2m58s

Reviewed-on: https://home.schmelczer.dev/git/git/andras/schmelczer-dev/pulls/75
This commit is contained in:
Andras Schmelczer 2026-05-28 16:20:12 +01:00
parent 0be50b6c24
commit b554e92e9f
83 changed files with 2995 additions and 723 deletions

View file

@ -1,6 +1,6 @@
---
title: Syncing State with Immutable Tries
description: How a multi-device life tracking project used trie structure to diff, reconcile, and synchronise goal state.
title: Syncing State with an Immutable Trie
description: 'A visual goal tracker whose lasting idea was the sync model: an immutable trie so structural diffs are trivial and only deltas cross the wire.'
date: 2026-05-05
projectPeriod: 'August-September 2019'
thumbnail:
@ -9,9 +9,9 @@ thumbnail:
tags: ['systems', 'web', 'tools']
featuredOrder: 4
role: Full-stack author
stack: ['Python', 'Angular', 'State synchronisation']
stack: ['Python', 'Angular', 'TypeScript', 'Custom sync protocol']
scale: Multi-device goal and task state shared between clients and a server
outcome: A working synchronisation model built around immutable trie properties
outcome: A working sync protocol where structural sharing made the delta tiny
audience: recruiter-relevant
links:
- label: Source
@ -20,35 +20,39 @@ media:
- type: image
src: ./_assets/towers.jpg
alt: Screenshot of a life tracking web interface represented with tower-like visual structures.
caption: The visual idea was simple; the useful lesson was the synchronisation model behind it.
caption: The interface was a 2019 weekend experiment. The trie underneath aged better.
---
Life Towers was a multi-device goal and task tracker with an intentionally visual interface. The surface idea was an aesthetic representation of previous and current goals. The more interesting part was synchronising state across clients without sending more data than necessary.
In August 2019 I wanted a goal tracker I'd actually open, on whichever device was nearest, without watching it disagree with itself. Nothing off the shelf fit, so I built one over a couple of weekends. The tower metaphor was the part friends saw; the part that aged well was the sync model that fell out of needing the same state in three places at once.
This was not a large distributed system, but it had a real version of a common problem: clients and server drift apart, and the system needs a compact way to compare, reconcile, and update.
## The problem in one paragraph
## The Problem
Pick any non-trivial mutable object graph, sync it across devices, and you end up either sending the whole thing on every change (wasteful) or writing ad-hoc diff logic per shape (brittle). I wanted a representation where the _shape_ of the data made the diff fall out for free.
If a task model is stored as an ordinary mutable object graph, synchronising it often becomes a choice between sending too much data or writing complicated ad hoc diff logic.
## The trie, concretely
I wanted a structure where the shape of the data made synchronisation easier. The client should be able to compare its state with the server's state, find a difference, reconcile it, and send only the delta.
A goal in Life Towers is a path of strings. `Health / Running / 5k`. Tasks under a goal hang off the leaf. A user's whole state is a tree, and a trie is exactly the data structure that makes that tree's _identity_ manipulable.
## Design
Two properties did the heavy lifting:
I used a trie. A trie made the hierarchical shape explicit, and its properties made it easier to reason about differences between stored versions.
- **Structural sharing.** When you tick off a task under `Health / Running / 5k`, the new root reuses every untouched subtree by reference. The `Career` branch and the `Reading` branch are the same objects they were before. Comparing the old and new roots is mostly pointer equality; only the path that actually changed gets walked.
- **Immutability.** Updates produce new structure instead of mutating. "Where I was" and "where I am" become two pointers, not two snapshots. The diff between them is whatever's not shared, and that walk is O(changes), not O(state).
The immutable nature of the structure simplified much of the logic. Instead of mutating arbitrary branches in place, updates could produce new structure with shared unchanged parts. That made reconciliation easier to reason about and reduced the amount of data that needed to move across the network.
The sync loop falls out:
The project also gave me a reason to deepen my Python and Angular knowledge, but the synchronisation structure was the main lesson.
1. Client holds the last root the server acknowledged plus its own current root.
2. To send: walk only the unshared paths, emit one op per changed leaf. In practice that's a handful of bytes for a typical edit, no matter how large the rest of the tree is.
3. Server applies, returns its new root.
4. Client rebases any in-flight edits by replaying them on top.
## What Worked
There's no conflict resolution layer because the operations commute on the structure. Two clients adding tasks under different branches produce non-overlapping deltas that compose trivially. The hard cases (two clients editing the same leaf) are tiny and obvious, because they're the _only_ place the deltas touch the same path.
The biggest win was choosing a data structure that matched the problem. Once the state was represented in a way that made comparison natural, the network protocol became simpler.
## What I'd change
The other useful lesson was that visual products still need a strong internal model. A pleasant interface is fragile if the underlying state is hard to trust.
- **Property tests around the rebase.** The reconcile path is exactly where a generator finds bugs that hand-written tests never think to write. I had hand-written cases; I'd start with `proptest` now.
- **A standalone spec for the wire format.** The part worth lifting out was the protocol, not the goal tracker. A short spec would let me (or anyone) reimplement it in a different stack without re-deriving everything from the Python source.
- **Strip the visual experiment.** The tower visualisation was fun but it bound the storage to a UI metaphor. The sync model should be a library; the towers should be a separate toy.
## What I Would Change
## If you take one idea from this
Today I would document the sync protocol more formally and add property-based tests around reconciliation. Synchronisation code is exactly the kind of code that benefits from generated edge cases.
I would also separate the visual experiment from the state synchronisation story more explicitly. The latter is the part that aged better.
Most sync problems are diff problems pretending to be transport problems. Pick the data structure that makes the diff free, and the protocol almost writes itself. The corollary: if you're writing a lot of "if this changed, send that" code, you're using the wrong structure.