Decision Records for solo developers

#practices #documentation #decision-records #karkhana #solo-dev

I started keeping Decision Records for my personal infrastructure last month, and I’m now convinced solo developers need them more than teams do, not less.

The conventional wisdom is the opposite. Decision Records — sometimes called ADRs (Architecture Decision Records) — are usually framed as a team practice. The standard argument: when a team makes a decision, you write it down so the people who weren’t in the meeting can understand it later. By that logic, a solo developer doesn’t need them. They were in the meeting. They were the meeting.

This argument is wrong, and the reason it’s wrong is that future-you is not present-you.

The problem

Six months ago I made a decision about my home server’s TLS setup. I tried Caddy, fought it for hours, gave up, and switched to Tailscale’s built-in serve. At the time the reasoning was vivid: I’d just lost an evening to permission errors and certificate cache bugs. The pain of Caddy was very recent and very specific.

Today, six months later, all I remember is “I didn’t use Caddy.” I don’t remember why. If I went looking at my setup tomorrow and asked “should I switch to Caddy?”, I would have to either rediscover the original problem from scratch, or trust a vague feeling that I had a reason once. Both options are bad.

A Decision Record is the artefact that solves this. Not for a team. For me, six months later.

The template

The format I’ve settled on, simplified:

## DR-NNN: [Short title]

Status: Accepted | Rejected | Superseded
Date: YYYY-MM-DD

Context: What problem are we trying to solve? What's the situation
that forced a decision?

Options considered: All real alternatives. At least two. If you
only had one option, you didn't have a decision; you had a constraint.

Decision: What we picked.

Rejected because: For each rejected option, why it didn't fit. This
is the most important section. It's the one that prevents future-you
from re-litigating the same choices.

Consequences: What this unlocks. What it costs. What we'll need to
revisit later.

The structure is rigid on purpose. When I’m writing one, the rigidity forces me to articulate things I’d otherwise gloss over. When I’m reading one, the structure means I can scan for what I need without reading the whole thing.

Why “Rejected because” is the most important section

Most people, writing about a decision they made, focus on why they made it. That’s natural. It’s also less useful than you’d think.

Six months from now, I won’t be questioning the decision I made. I’ll be questioning whether I should change it. The relevant question is not “why did I do this?” — that’s already done. The question is “should I keep doing this, or do the alternatives look better now?”

Answering that requires knowing what the alternatives were and why they lost. If a DR only documents the winning option, future-you will look at the world fresh, see one of the rejected alternatives, and think “huh, that looks promising, why didn’t I try that?” You’ll re-evaluate from scratch and possibly arrive at the same conclusion six months and many hours later.

The “Rejected because” section short-circuits this. Future-you sees the alternative, sees a paragraph explaining why it didn’t fit, and either accepts the past reasoning or finds a specific point where the world has changed since. Either way, the analysis is fast.

I cannot stress how much this matters. Most of my own DRs are 70% “Rejected because” and 30% everything else.

A real example

Here’s an abbreviated DR from my own infrastructure docs:

DR-004: Tailscale serve over Caddy for private TLS access

Status: Accepted (after extended attempt at the alternative)

Context: Want HTTPS access to private services from inside my tailnet. Original plan used Caddy with a Cloudflare DNS-01 wildcard cert.

Options considered:

  • Caddy with wildcard TLS via Cloudflare DNS-01
  • Tailscale serve with *.ts.net cert handled by Tailscale
  • Tailscale Funnel (rejected immediately — exposes publicly)

Decision: Tailscale serve. Use machine.tailnet.ts.net for tailnet access. No custom domain on the tailnet.

Rejected because:

  • Caddy worked end-to-end through cert issuance — wildcard cert obtained successfully. But running Caddy on macOS surfaced cascading permission issues. Port 443 binding requires root. LaunchAgent vs LaunchDaemon: macOS 15 SIP blocks unsigned LaunchDaemons; LaunchAgent runs as user but then can’t bind 443 without escalation. When Caddy ran as root, cert storage in user home dir had restrictive permissions root couldn’t read. Even after fixes, Caddy’s autosave path was sticky and the runtime cert cache never loaded the cert. TLS handshake failed silently. Total time invested in fighting this was hours, with no end in sight.

Consequences:

  • Tailscale serve is free, automatic, and bulletproof.
  • Trade-off accepted: no custom domain for tailnet services. Aesthetic, not functional.
  • The Cloudflare API token created for Caddy is now unused but left in shell config in case a future use comes up.
  • Custom domain reserved for genuinely public services via Cloudflare Tunnel.
  • Lesson: if Tailscale already provides TLS for tailnet-only services, don’t fight macOS for the same thing with custom domain.

That last line — the lesson — is what I actually want to remember in six months. The rest is the supporting evidence so the lesson is grounded, not vibes.

When to write a DR

The threshold I use: write a DR when a decision isn’t obvious in retrospect.

If I’m picking between Postgres and MySQL for a project where Postgres is the obvious choice for me, I don’t write a DR. There’s nothing to record. If anyone asks why Postgres, the answer is “always Postgres unless reason.”

If I’m picking between Coolify and Dokploy for a specific role, that’s a DR. Both are good. The reasons one wins are not obvious. Future-me might wonder.

Other triggers:

  • I considered something and rejected it for non-obvious reasons. (DR captures the why.)
  • I made a choice that explicitly rules out a future option I might want. (DR captures the trade-off.)
  • The decision feels surprising or contrarian. (DR explains it to future-me before I question it.)
  • I changed my mind from an earlier decision. (DR supersedes the old one.)

I do not write DRs for:

  • Pure preference. “I use vim, not emacs” doesn’t need a DR.
  • Defaults I follow. “Use HTTPS” doesn’t need a DR.
  • Things that are too small to matter. “Use 2-space indentation” — pick one in your linter config and move on.

How DRs interact with documentation

A DR is not documentation. Documentation tells you how to do something. A DR tells you why the something is done that way.

If you only have docs, you don’t know which parts are the result of careful thought and which parts are accidents you’ve never reviewed. Adding DRs gives the docs an intentionality layer underneath. When I update something in my setup, I check whether any DRs are affected. If so, I either update them, supersede them, or note that the change disregards the previous reasoning intentionally.

This sounds like overhead. In practice it adds about 10 minutes per non-trivial change, and it has saved me hours every time I’ve come back to part of my system after a break.

The cost

Writing a DR for every meaningful decision takes about 15 minutes. I’ve written about a dozen of them across my home server setup. That’s three or four hours total.

In return, I have:

  • A document that lets future-me make changes confidently rather than tentatively.
  • A document that lets me explain my system to someone else (or to a fresh AI assistant context) in seconds, by pointing at the DRs instead of re-explaining.
  • A document that catches contradictions when I’m about to make a new decision that conflicts with an old one.

The investment pays back the first time I revisit any part of my system after a month away. That’s a high-value, low-effort discipline, which is exactly the kind of thing solo developers should be hunting for.

How to start

If you’re solo and don’t currently keep DRs:

  1. Make a folder. Anywhere. Call it decisions/.
  2. Next time you make a decision that isn’t obvious, write a DR for it before you implement.
  3. Use the template above. Don’t optimize the template until you’ve written five DRs.
  4. Number them. Even if you only have three.
  5. Don’t go back and write DRs for past decisions you’ve already made unless you’re actively reviewing them. The cost is high, the value is low — past-you isn’t going to read them, and you’ve already lost the freshest reasoning.

The hardest DR is the first. After that, the format becomes natural. Your first DR will probably be too long. Your tenth will be the right length.

This post is part of the Karkhana series, about running solo software work sustainably with high-leverage AI agents.