Jul 20, 2026
No database, on purpose
Why Ladderline stores promotion evidence in plain markdown files instead of a database, and the one place that decision still costs something.
A note that says “pushed back on the caching design, three teams ended up adopting it” is doing real work. Somebody’s case for a promotion might rest on four or five sentences like that, written down eight months before anyone reads them again. If the thing holding those sentences loses one, or quietly mangles it, or just becomes unreachable because a server went down at the wrong moment — that’s not a bug report. That’s someone’s case missing a paragraph they can no longer reconstruct from memory.
I built Ladderline to fix the reconstructing-from-memory part. Log evidence the moment it happens, tagged against your team’s actual ladder, so review season is assembly instead of archaeology. Getting the storage layer wrong felt like the one mistake that wouldn’t be recoverable after the fact — lose someone’s evidence and there’s no re-running the interview that generated it.
My first instinct, if I’m honest, was Postgres. Or SQLite, if I wanted to keep the footprint small. A table for people, a table for notes, a foreign key tying each note to a cycle and a competency — the kind of schema you can sketch in four minutes and feel very sure of. Databases are good at exactly the things this project needed on paper: structured records, queries, integrity constraints you get for free. Talking myself out of that felt a little perverse at first.
What actually killed it was thinking about what the file even represents. This isn’t application state that only matters while a process is running — it’s a record about a real person’s work, sitting on that person’s manager’s laptop, that needs to survive the tool itself disappearing. A .db file is a black box you need the right client and the right schema knowledge to even look inside. A markdown file with a little YAML frontmatter at the top is something you can open in Notepad, grep, diff, put under version control, or read five years after Ladderline stopped being maintained. For evidence that’s meant to be trusted, “you can just look at it” turned out to matter more than “you can query it efficiently.”
So every note is one .md file — a folder per person, a file per entry, frontmatter for the tag/date/cycle, the note text underneath as plain prose. No install step beyond the CLI itself, no daemon, nothing running that isn’t the command you just typed.
The part I actually spent the most effort on wasn’t the file format, though — it was making sure the logic sitting on top of those files couldn’t quietly rot. Every function that touches a workspace lives in one layer that does exactly two things: read or write files, and either return data or throw one of about seventeen specific error types. Nothing in that layer prints to a terminal, asks a question, or knows whether it’s being called from the CLI or the local dashboard. Ninety-nine tests exercise that layer directly, and every single one of them creates a real temporary directory on disk and writes real files into it — nothing’s mocked, because the whole point was proving the files actually end up correct, not that some stand-in object got called with the right arguments.
Where this genuinely costs something is referential integrity, which a database hands you basically for free and files don’t. A note’s cycle field is just a string — 2026-Q1 — sitting in that note’s frontmatter, with no actual link back to the cycle definition living in a separate cycles.yaml. Delete that cycle with a real database behind things and a foreign key either blocks you or cascades the deletion automatically, no extra code required. Delete it here and nothing stops you — the notes still happily say cycle: 2026-Q1 forever, pointing at something that no longer exists anywhere else.
The fix ended up being: cycle remove has to walk every tracked person’s notes itself, count how many still reference the cycle being deleted, and print an honest warning before doing anything — something like “12 notes still reference this cycle, they’ll keep it as historical data.” Not blocking, since old evidence shouldn’t become unreachable just because a cycle got cleaned up, but not silent either. It’s maybe fifteen lines of code. A database would have made that fifteen lines unnecessary, or at least made it declarative instead of something I had to remember to write by hand. That’s the actual price of the plain-files choice — not a big one, but a real one, and the kind of thing that only shows up once you’re already deep into deciding files were the right call for everything else.
Comments
Loading comments…