Chapter 5 of 6 · 9 min read
A record with no assumptions cannot be reopened
On this page
The renewal notice arrived with a 5% uplift and a request for a decision within three weeks. The engineer who picked it up had joined eight months earlier, and her first move was the right one: find out why the company chose this vendor in the first place. The repository had an architecture decision record — an ADR, one of the short numbered files a team commits next to its code to say why a choice was made — at docs/adr/0014-retrieval-layer.md, dated 11 March 2024, and it read in full: "We chose Vendor X because it is the industry standard and the team already knows it."
Both clauses were true when they were written and neither is checkable now. "Industry standard" was a description of a market that has since produced two credible entrants and one acquisition; "the team already knows it" referred to a team of nine, of whom four remain. Neither clause says what the vendor was chosen to do, what else was looked at, at what volume or price the choice made sense, or what would have to change for it to stop making sense. Faced with a renewal at three times the original traffic, she had a conclusion with no working, which meant her only options were to renew on faith or to redo eighteen months of analysis from scratch in three weeks.
What a reader in two years actually needs
The gap between what that sentence records and what a later reader has to answer is specific, and it is worth laying side by side, because the missing pieces are always the same four.
| The sentence records | The question two years later | What was needed in the record |
|---|---|---|
| A conclusion | Was it chosen for what we now use it for? | The requirements, with thresholds |
| "Industry standard" | Were the alternatives real, or was this the only one considered? | The options, scored against those requirements |
| "The team already knows it" | Does the reasoning survive the team that held it? | Assumptions, with values and dates |
| Nothing about the future | Has anything changed enough to reopen this? | Triggers, with owners |
Requirements matter because the vendor was almost certainly chosen for a narrower job than it now does. Alternatives matter because a decision where nothing was rejected is not a decision, and knowing that the runner-up lost on hybrid search rather than on price tells a later reader exactly which market development would change the answer. Assumptions matter because they are the part that expires; the analysis in chapter 1 was entirely a set of assumptions with numbers on them, and none of those numbers is durable. Triggers matter because without one, reopening a decision requires somebody to volunteer to reopen it, and that person will be arguing against a colleague's judgement rather than against a condition everyone agreed to in advance.
The format, and why it needs a section the original did not have
Architecture decision records in this shape come from Michael Nygard's 2011 note, Documenting Architecture Decisions, written against the failure mode of the large architecture document: a single living page that everyone was supposed to keep current, which drifted until it described a system nobody was running. His alternative was a numbered sequence of short, immutable files, each capturing one decision with its context and consequences, superseded by a later file rather than edited. The immutability is the whole trick, because it turns the archive into a history you can read forward and see how thinking changed.
Build-versus-buy decisions need one addition to that template. Nygard's records mostly capture internal design choices, which stop being live once the code is written; a purchase decision stays live for as long as the invoice recurs, and the ground under it is being moved by a third party who is repricing, shipping features and occasionally being acquired. So the record needs a section the original form does not have — the conditions under which it should be reopened, expressed as things that can be measured rather than as a date in a calendar. "Review annually" produces a meeting where everyone agrees nothing has changed; "reopen if monthly ingested vectors exceed 8 million" produces a review at the moment the arithmetic actually changed.
Here is the record that should have been written in March 2024, for the same decision.
# 0014 — Retrieval layer: managed vector database (Vendor X)
Status: accepted
Date: 2024-03-11
Deciders: A. Okafor (eng lead), R. Bell (platform), M. Haas (product)
## What we need it to do
- Hybrid dense + lexical search over contract text, with metadata filters
pushed into the query (tenant, doc_type, effective_from).
- p95 retrieval latency < 60 ms at recall@10 >= 0.90 on the 800-doc golden set.
- 4M chunks today, forecast 6M in 12 months.
- Operable by the two backend engineers who own the ingestion pipeline;
we have no index specialist and are not hiring one.
## Options considered
| Option | Hybrid | Filter push-down | p95 @ recall 0.90 | 12-mo cost | Ops burden |
| --- | --- | --- | --- | --- | --- |
| Vendor X (managed) | native | yes | 41 ms | £40,800 | vendor |
| Vendor Y (managed) | fuse ourselves | pre-filter only | 38 ms | £33,600 | vendor |
| pgvector on our RDS | lexical via tsvector | yes | 145 ms at 4M rows | ~£9,000 infra | ours |
| Self-hosted OSS engine | native | yes | 44 ms | ~£12,000 infra | ours, ~0.5 FTE |
Rejected pgvector on latency at our corpus size; rejected self-hosting because
0.5 FTE of index operations is 1/18th of engineering for a capability that is
not differentiating (see 0011). Vendor Y is cheaper and lost on hybrid fusion,
which we would have had to build and tune ourselves.
## Decision
Buy Vendor X for 12 months, billed monthly.
## Assumptions (the parts that expire)
- A1: corpus reaches ~6M chunks by 2025-03; pricing tier holds to 10M.
- A2: annual spend £40,800 at forecast volume; overage £0.09 per 1k vectors.
- A3: no vendor offers customer-supplied hybrid weighting on a cheaper tier.
- A4: retrieval quality is not a differentiator; extraction accuracy is.
## Exit cost, estimated today
10–14 engineer-weeks plus ~6 weeks dual running (~£20k licence overlap).
Mitigations in place: chunks and chunker version stored in our own S3;
golden set owned by us; query construction confined to `retrieval/engine.py`.
## Reopen this if
- T1: monthly ingested vectors > 8M (A1 broken) — owner: platform on-call
- T2: annual run-rate > £60,000 (A2 broken) — owner: eng lead, monthly finance export
- T3: any credible vendor ships tunable hybrid weighting below £30k/yr (A3)
- T4: p95 retrieval > 90 ms or recall@10 < 0.85 for two consecutive weeksThat is roughly a page, it took about ninety minutes to write, and every line in it is something the team knew on the day. The options table is not research anyone did for the record; it is the benchmark they had already run. The exit estimate is the paragraph chapter 3 argued for. The assumptions are the numbers that justified the choice, written down at the moment they were still true.
Two details in it do disproportionate work. The requirements carry thresholds rather than adjectives — "p95 under 60 ms at recall@10 of at least 0.90 on the 800-doc golden set" rather than "fast enough retrieval" — which means a later reader can run the same measurement and get a comparable number. A requirement without a threshold cannot be failed, so it cannot rule an option out, which is how records end up with a rejected-options table where nothing was actually rejected. And the constraint about staffing is in the requirements rather than buried in the reasoning, because "we have no index specialist and are not hiring one" is the single fact that eliminated two of the four options, and it is precisely the sort of thing that changes without anyone updating a document. When the company hires a search engineer in 2026, that requirement is false and the decision deserves another look.
The other thing worth noticing is what the record does not contain. There is no discussion of how retrieval works, no schema, no sequence of calls, and no description of the ingestion pipeline. All of that belongs in the system's own documentation and changes on a different clock; mixing the two produces a document that is stale within a quarter, which is the failure the format was invented to avoid.
Writing it at the moment of the decision is the part people skip, and the substitute — reconstructing it a month later, or worse, asking someone to write records retroactively for decisions already made — produces a different artifact entirely. Reconstructed records contain the reasons that survived, which are the ones that sounded best in hindsight, and they lose the assumptions, because assumptions are invisible to the person who still holds them. The ninety minutes has to be spent while the alternatives are still live.
Triggers with owners, or the section is decoration
A trigger that lives only in a markdown file is a good intention. The version with teeth is one where each condition is a query somebody or something evaluates on a schedule, and where firing produces a ticket rather than a feeling.
# ops/decision-triggers.yaml — evaluated monthly by a scheduled job
- record: adr-0014
id: T1
assertion: monthly_ingested_vectors <= 8_000_000
query: >
select sum(vectors_upserted) from index_stats
where day >= date_trunc('month', now())
owner: platform-oncall
on_break: open_issue
- record: adr-0014
id: T2
assertion: trailing_12m_spend <= 60_000
query: >
select sum(amount_gbp) from vendor_invoices
where vendor = 'X' and issued_at > now() - interval '12 months'
owner: eng-lead
on_break: open_issueTwo of those four triggers can be automated because they are numbers a system already holds. T3 cannot: no query tells you a competitor shipped a feature, so its owner is a person with a recurring calendar entry and a fifteen-minute job of checking three changelogs. Writing that down as a person's task rather than pretending it is monitorable is the honest version.
The lifecycle this creates is small enough to hold in your head, and the part that distinguishes it from a wiki page is that a record is never edited in place.
Reaffirming is a real outcome and deserves its own path, because the common result of a triggered review is that the decision still stands under revised numbers. What must not happen is editing 0014 to say something different; the new thresholds go in a short 0027 that supersedes it, so that the archive still shows what was believed in 2024 and why it changed.
A reaffirmation is short — a paragraph saying which trigger fired, what the current values are, why the decision stands, and what the new thresholds are — and it is the outcome to expect most of the time. That matters for how the mechanism is received, because a team that believes a triggered review means a migration will avoid setting triggers. The review is a re-measurement with three possible endings, and the boring one is the usual one.
Getting the record read at the moment it matters
A record nobody opens is indistinguishable from no record, and the failure is almost never that people refuse to read; it is that nothing puts the document in front of them at the moment they need it. Three cheap placements fix most of it. The vendor's entry in whatever system holds contracts and renewals carries the record's path, so the person handling a renewal notice lands on it without searching. The module that talks to the component carries a one-line reference in its header comment — # see docs/adr/0014 above retrieval/engine.py — so an engineer wondering why the query layer is confined to one file finds the reason attached to the constraint. And the trigger job that opens an issue puts the record's link in the issue body along with the assertion that failed and its current value, so the ticket arrives with its own context.
Ownership is worth stating too. Each record names deciders, and one of them holds it: the person who answers when a trigger fires and who presents the assumptions at the review. That is a small standing responsibility, on the order of an hour a quarter, and it is the difference between a review that has someone to run it and a review that waits for a volunteer. When that person leaves, reassigning their records is part of the handover in the same way as reassigning services.
Where this stops being useful
A decision record is not a design document. It does not describe how retrieval works, and anyone trying to learn the system from the ADR sequence will be misled, because records capture forks in the road rather than the road. It is also not an approval gate; a record written to satisfy a governance checklist gets filled with defensible generalities, which is how you end up with "industry standard" again in a longer form.
Not every decision earns one. The test is reversibility: if putting the choice down again costs a fortnight or more, it is worth ninety minutes now, and if it costs an afternoon, it is not. That rules out most library choices and rules in every recurring vendor relationship.
The rejected options section carries a particular obligation, which is to state the strongest version of what was turned down, including who argued for it. A record that describes the runner-up in terms its advocate would not recognise is worse than one that omits it, because a later reviewer will read a fair-looking comparison and conclude the ground was covered when it was not.
The honest limitation is that the median decision record is written once and never opened again. Format does not fix this; the trigger wired to a scheduled job is the only mechanism here that acts without someone deciding to act. Even then, the trigger only produces a ticket, and what happens next is a conversation between people, one of whom made the original purchase and may not enjoy watching it be reconsidered. The next chapter is that conversation, with the spend at four times its forecast and a proposal already written.
Build vs buy across the stack — read the current version at https://daviddominguez.dev/learn/en/build-vs-buy-across-the-stack/a-record-with-no-assumptions-cannot-be-reopened