ADR-005 — Synchronous I/O and a mandatory snapshot
Async reading measured 42 ms slower than sync, and the snapshot made the read 23× faster.
Date: 2026-09-02 · Status: accepted · Scope: architecture
Context
ADR-002 removed YAML as the cause of slow reads. What remained was whether that alone fits inside 200 ms without a snapshot cache.
Measured on 10,000 JSON events spread across 12 monthly folders (best of 3):
| How the journal is read | Time |
|---|---|
Synchronously (readFileSync in a loop) | 140 ms |
Asynchronously, concurrency 256 (Promise.all) | 182 ms |
| One compacted file | 6 ms |
Two things are worth noticing.
Async reading turned out slower than sync. Counterintuitive, but simple to explain: the files are tiny and already in the page cache, so promise overhead outweighs any gain from concurrency. Making the core async "because that is proper" would have meant paying 42 ms to get worse.
JSON alone does not save it. 140 ms of reading plus 40 ms of Node startup is 180 ms out of 200. The guardrail is formally met with a 10% margin, and that margin disappears on the first repository larger than the test one.
The compacted file gives 6 ms — 23× faster, and 1.7 MB on disk against 39 MB.
Decision
The core is synchronous. A CLI is a process that does one thing and exits; there is nothing to avoid blocking.
state.json is mandatory, and gitignored. Invalidation is by the ULID of the
last event: the snapshot stores the id of the newest event it accounted for, and
rebuilds if the journal holds a newer one. This is where choosing ULID in ADR-003
pays off — "newer or older" is a string comparison and owes nothing to clocks.
The rule that cannot be broken: state.json is never a source of truth.
Deleting it must always be safe, and there is a test that asserts exactly that.
Consequences
- 6 ms instead of 180, budget headroom for real repositories, and synchronous code that is markedly easier to read and test.
- Derived state that can drift from the journal — a whole class of bug that would not exist without a cache. Every "it shows the wrong thing" report starts here.
- What would make us revisit it: at hundreds of thousands of events, rebuilding the cache becomes slow in itself, and the answer is incremental reads — only events newer than the snapshot — rather than folding the whole journal.