9 min read

Code got cheap. Architecture got iterative.

Engineering

Building software (August 2026)

This post describes how we build software as of August 2026. Our engineering practices look almost nothing like how we used to build software just two years ago, and we have no expectation that this approach survives the next two years intact. That’s one of the reasons to write it down. A snapshot captures what we are doing now, where an account written later would smooth over the parts that haven’t settled.

Two years ago implementation time was a lot more scarce. You held a design in your head and the work was getting it coded, tested, and shipped. That is no longer where the time goes. When an agent can produce a working implementation in an afternoon, the constraint moves to everything the code depends on and doesn’t contain: why a subsystem is split the way it is, what existing architecture constraint made the obvious approach wrong, which plausible designs we already tried and abandoned.

Frequently that knowledge lived in the heads of the people who were around when those decisions were made (you know, the people who know where all the bodies are buried), and leaked out through code reviews one comment at a time. That works when the reviewer holds the whole history and has time to look at every PR. It doesn’t work when the thing writing the code, as competent as it is, has no historical knowledge about the product. It will produce something reasonable, correct in isolation, and quietly at odds with three decisions nobody wrote down.

So we are writing them down. Our ADRs (Architecture Decision Records) live in docs/adr/ in the main repo, the oldest from December 2025. They are the first artifacts of any significant change, and they are the first thing an agent reads before it writes a line of code.

The second effect of cheap code took us longer to notice. When implementation was the expensive part, an architecture decision was almost a one-way door. Getting one wrong meant weeks of migration work, so the rational move was to deliberate up front and keep scrutinizing the choice well past the point where it was clearly sound enough to try. That calculation no longer holds. The architecture is now something we can iterate on, in roughly the way we used to iterate on code.

ADR

All major changes start with an ADR. We use a slightly modified MADR 3.0 format. The choice to use it is itself one of the ADRs, which is either good discipline or a joke that got away from us.

Architecture decisions can be broad ones that shape a large part of the system, or narrow ones that cover a single piece of functionality. Here is one of each.

Our observability pipeline ADR is the cross-cutting kind: about 130 lines that fix the shape of the entire event path, from how events are collected, through where they go for long-term storage, to the consumers hanging off the stream, how the data gets queried, where encryption is applied, and what someone has to do to add a new event type later. Most of what we’ve built on that pipeline since has followed the patterns that ADR established.

Self-service account creation is the narrower kind, covering one flow from signup to a working tenant. The part of it that stays highly relevant is the Decision Drivers section, because it records why we were building it and what we were actually optimizing for. All subsequent first-time-user-experience features carried these decision drivers forward.

What stays out of the ADR matters as much as what goes in. Implementation details belong in the execution plan, not the decision record. What belongs in the ADR is the decision and the constraints around it. The test we use is whether you could change the value without revisiting the decision. If yes, it’s a plan detail. If changing it would invalidate the rationale, the constraint belongs in the ADR, but often the specific value still doesn’t.

Review

The author drafts the ADR, usually with Claude, working from an internal skill that carries our quality criteria and knows which parts of the codebase to read for a given decision category.

Then the same criteria get pointed back at the draft. Claude reviews it in a separate pass: which criteria pass, which don’t, cited against specific text, plus a verdict of ready to merge, needs minor revision, or needs significant revision. It suggests changes rather than rewriting, because the point is to hand the author a list of gaps, not a new document they haven’t thought through.

That pass catches the mechanical failures cheaply: the single option considered, the vague context, the missing negative consequences. What lands in front of human reviewers is a draft that’s already survived the checklist, so peer review spends its attention on the part that needs judgment.

Human review only works if the ADR is short enough to actually get read. This is the constraint that caught us out, because it is the one place where the AI and the humans are not interchangeable. An agent will read all 500 lines of an ADR. A person will skim them. Past a certain length what comes back is approval rather than review, and an approved ADR that nobody argued with is worse than no ADR at all, because it now carries the authority of having been reviewed.

The cheapest gate runs before either kind of review. A linter walks every ADR and checks that it declares a valid status and date, and that every required section is present: context, drivers, considered options, outcome, consequences with both a positive and a negative subsection, and more information. A new ADR missing any of them fails the build. It’s just another test running on every pull request.

Implementation

From an accepted ADR, one of two things happens.

Claude implements small and well-scoped ADRs directly from the record.

For larger or cross-repo ADRs, we generate an execution plan first, written to docs/adr/plan-<slug>.md. The plan holds the detail the ADR deliberately excluded, such as contract changes, ordered phases with verification steps, migration ordering across two migration tools, testing strategy, rollout, and a rollback path for every phase. Several specialist agents draft it in parallel. There is always an implementation architect, plus integration, reliability, security, or infrastructure experts depending on what the decision touches. At least two synthesis passes reconcile their output before anything gets written. Then Claude implements the plan.

The person who wrote the ADR owns the result. Not “reviewed the diff”. Owns it. They are accountable for whether the code the agent produced actually does what the decision said, whether it’s high-quality code, and whether it fits the system. Delegating the writing of code doesn’t delegate the responsibility. After that it goes through our normal code review and gets merged like anything else.

The feedback loop

The ADR doesn’t get archived when the PR merges. It stays in the repo, and every future session reads it. That turns it from documentation into something load-bearing. New code gets written against it.

That’s the compounding part. When we ask Claude to design the next feature, it has the whole set of ADRs describing how this system is put together and why.

It knows how the observability pipeline is shaped: events archive to S3 in Parquet for long-term analysis, and a separate short-term store holds a few months of the same data for fast queries and alerting, because the archive is too slow to sit behind a dashboard. So it puts a new event where the existing tiers say it goes, rather than inventing a third path.

It knows we support federated identity only, and that email-and-password signup was considered and rejected specifically so we would never own password complexity rules and password resets. So it doesn’t offer to build a login form. It also knows the condition we wrote down for revisiting that, which is more than most engineers would remember a year later.

It knows which decisions amended earlier ones. The output stops being generically reasonable and starts being consistent with the system we actually have. And when it proposes something that contradicts a prior decision, it says so and cites it, which is exactly the conversation we want to be having.

ADR Updates

ADRs evolve so we treat ours as working files.

Some get revised in place. When the implementation teaches us that the decision was wrong about something, we append a dated revision to the record rather than editing the original text. The file then shows both what we believed at the time and what we learned, which is more useful than a clean version that has quietly been rewritten. One of our longer records carries several of these, including one whose entire job is to list claims the implementation turned out not to match.

Others get amended by a later decision. A new ADR narrows or replaces part of an earlier one, both records link to each other, and the index notes the relationship so nobody reads the older one without knowing it moved. Deprecated and superseded are real statuses in our process rather than decoration, and using them requires updating both sides of the pair.

Iterative architecture wasn’t as practical when re-implementing was the expensive part. Now the cost of having been wrong is much closer to the cost of writing the ADR in the first place. That changes the incentive at the front of the process: we would rather decide, write it down, ship it, and revise it than spend another week trying to be right in advance.

The discipline that keeps this from becoming drift is that the revision has to be written down too. An undocumented change of mind is indistinguishable from nobody having decided. So the bar for changing a decision is the same as the bar for making one. Capture what changed and why it changed.

Open Items

This isn’t a finished process, and the failure mode is specific enough to name.

Some ADRs Claude nearly one-shots. The implementation comes back only needing tweaks, and ships extremely quickly. Others come back with breaking changes buried in them: a contract altered where it should have been extended, a migration ordered wrong, a component refactored well past what the decision called for.

Part of that is where the models are today. But a meaningful part is us. The ADRs that go badly tend to share traits. Scope too broad, so one record contains three major decisions. Context that assumes knowledge the reader would have but the record doesn’t hold. A Decision Outcome that describes the goal without pinning the boundaries the implementation must not cross. When we write a tighter ADR, we get a better implementation. The link is strong enough that a bad implementation now sends us back to the record before it sends us anywhere else. Usually it didn’t say what we thought it said.

We’re still working out which decisions are safely one-shot, which need a plan, and how much of the difference is model capability versus our own writing. We’re keeping score. Once we can define the rules, they’ll go in docs/adr/ like everything else.