August 2025
What you can't add later
I have deleted three abstractions I was sure I would need, and only one of them actually had to be deleted, which taught me the question to ask before building one is not whether I will need it but what being wrong will cost.
There is a tag in one of my repositories called pre-rebuild, and it marks the last commit of a system I built completely and then threw away. About twenty thousand lines: a conflict-free replicated data type for the document model, a six-state machine for the control flow, a typed kernel of sixteen component types, a fourteen-tool surface for the agent that drove it. All of it worked. The commit that replaced it removed 21,671 lines and added 1,527, and started over on plain signals and a small relational document. I do not regret a single one of the things I deleted. They were not wrong. They were early.
That distinction took me a while to earn, because the easy lesson is the wrong one. The easy lesson is “don’t over-engineer,” and it is useless, because at the moment you build the abstraction it does not feel like over-engineering, it feels like foresight. The replicated data type was the correct way to let many people edit one document at once, and it is genuinely hard to retrofit, which is exactly the reasoning I used to justify building it first. But no one was editing together. There was one user, and the entire apparatus existed to serve a second user who did not exist and might never. I had paid, in complexity that touched every other decision, for an answer to a question the system had not asked.
What makes “premature, not wrong” more than a consolation is that I have since watched the same move play out two more times, and the three cases disagree in a way that is instructive. I built an entity-merge operator that proposed folding two records together when their names were similar, and switched it off a day later when it merged a count of one thing into a count of a different thing because the words overlapped. I built a graph retrieval layer on top of plain vector search, sure it would win on multi-hop questions over structured data, and it scored 15 of 18 against a plain baseline’s 15 of 18, identical question for question, zero lift from the graph. Here is the part that matters: I deleted the first system, but I did not delete those two. I switched them off and left them dormant, because leaving the seam in place cost almost nothing. One abstraction had to be ripped out. The other two could just be parked.
So the question I now ask before building an abstraction is not the one I used to ask. Not “will I need this,” because you cannot know, and guessing wrong is the default. The question is: if I am wrong about needing it, what does adding it later cost? Most abstractions, it turns out, cost a refactor. The replicated data type came out in a single commit behind an interface that never changed, because the document model was always the real abstraction and the replicated type was just one implementation of it that overdelivered. A closed component vocabulary can always be tightened later; an open one you adopt early is the cheap direction. The merge operator and the graph layer proved the point by not even needing deletion. Reversible abstractions do not have to be removed. They can be left dormant for almost nothing, which means building them early was a small mistake and keeping them was a smaller one.
Then there is the one kind you cannot add later at any price, and in all three systems it was the same kind: provenance. Where a value came from. The merge operator kept its full lineage even while it was switched off. The graph layer survived, demoted, as a record of where answers came from. The rebuilt system made its log of changes the source of truth precisely because that log buys you replay and audit and time-travel for free. You cannot reconstruct where a value came from after the fact. If you did not record it at write time, the information is simply gone, and no cleverness at the end recovers it. A replicated data type can be retrofitted. A paper trail cannot be back-dated. That is the asymmetry the whole heuristic rests on: refactor-cost abstractions you defer, rewrite-cost abstractions you build on day one, and the thing that is almost always rewrite-cost is the record of what happened.
I will not pretend the test makes me reliable, because it only fixes one of my two failure modes. It saves me on the reversibility axis, where I can usually tell a refactor from a rewrite. It does nothing for me on the need axis, where I am routinely wrong: I judged the merge operator foundational and it was disposable, I judged the graph layer a retrieval primitive and it was a no-op on the data I had. The taxonomy rescued me only because the cost-of-being-wrong axis is more forgiving than the will-I-need-it axis. And “cheap to add later” quietly assumes you kept a clean interface, which is not free either: an abstraction that leaks its API through the whole codebase stops being cheap to add or remove, and some of these cost me more than the rule predicted for exactly that reason. Three deletions are not a study. They are a heuristic I would still want to test, written down now mostly so the next time I am sure I will need something, I remember to ask the cheaper question first.