When Design Does Not Pay
Structure is an investment against future change. Where there is no future, it is pure cost — and knowing which code that is, is part of the skill.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
When is the right amount of design close to none?
A script to migrate 40,000 rows once, this weekend, and then never run again. A colleague asks where the tests are.
Every piece of code deserves the same standards. Tests, boundaries, error types, dependency injection — professionalism means consistency.
Standards are a means. Applied to code with no future, they cost real hours and return nothing, and the hours come out of work that does have a future.
- Standards are a means. Applied to code with no future, they cost real hours and return nothing, and the hours come out of work that does have a future.
- It also trains a team to see the practices as ritual rather than as tools with conditions — which is precisely how they get abandoned wholesale later, when someone notices they are not helping.
- And it is the wrong risk model. The risk in a one-shot migration is data loss on the day, not maintenance in a year; effort spent on structure is effort not spent on a dry run against a production snapshot.
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- It runs once. There is no second change, because there is no second run.
- It must be correct on the day — but correctness here is verified by checking the output, not by a suite.
- It must be safe to re-run if it fails halfway, because it will fail halfway (Idempotency by Design).
- It must not corrupt data it was not asked to touch.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- Whoever writes throwaway code owns making sure it is genuinely throwaway — which mostly means it cannot quietly become load-bearing.
- Whoever reviews it owns checking the risk that is actually present, which for a migration is the data, not the structure.
- The boundary is the code's expected lifetime and blast radius. Those two, not its size or its language, decide how much structure it earns.
What actually decides it
Two variables, and neither is size or language. How long the code lives, and how much damage it can do. Everything else — tests, boundaries, error modelling, injection — follows from where a piece of code sits on those two axes.
- The high-blast-radius rows earn verification effort even when they earn no structure, and those are different budgets.
- The prototype row is where most of this domain's real-world problems begin — not from ignorance, from a prediction that turned out wrong.
- Nothing on this list earns effort because of professionalism. Every row is a risk judgement.
| Code | Lifetime | Blast radius | What it actually earns |
|---|---|---|---|
| One-shot data migration | One weekend | High — production data | Dry run, transaction, resume point, row-count assertion, backup. Almost no structure. |
| Spike to answer a question | Days | None — never merged | Nothing. Answer the question and delete it. Structure here is pure waste. |
| Internal tool, two users | Years, changed rarely | Low | Readable code and a couple of tests on the tricky part. Boundaries would not repay. |
| Core domain module | Years, changed weekly | High | The full apparatus. This is the code the rest of this domain is about. |
| Prototype for a demo | Officially days | Officially none | The dangerous cell. Officially disposable, frequently permanent — decide *before* the demo what happens if it is well received (Deliberate Debt). |
| Generated code | Regenerated | Varies | Nothing by hand. Structure belongs in the generator, and edits to the output are lost anyway. |
Making the assumption survivable
The exemption rests on a prediction — this code will not live long — and predictions fail. The mature version does not avoid the prediction; it makes the failure cheap to detect and cheap to correct.
That means writing the assumption down where the code is, and making the structural cost of being wrong small: contained in one directory, importing production code but never imported by it, and impossible to schedule by accident.
- 1Notice
The code is run a second time, or something imports it.
fails by Nobody notices, because nothing was watching for it.
- 2Decide, once
Delete it, or promote it deliberately.
fails by Neither — it stays, unlabelled, and is now load-bearing.
- 3Promote properly
Move it out of
one-off/, add tests for the behaviour it actually has, name its errors.fails by Moving it without tests, so it looks like production code and is not (What "Legacy" Actually Means).
- 4Record the debt
If promotion is deferred, write down what is missing and the trigger to fix it.
fails by An intention instead of a record, which is how accidental debt is created (The Debt Register).
1// scripts/one-off/2026-08-backfill-tax-codes.ts2//3// ONE-OFF. Runs once, 2026-08-30, then delete this file.4//5// Not tested, not structured, deliberately. Correctness is6// verified by the dry run below against a prod snapshot, not7// by a suite. Blast radius is orders.tax_code only.8//9// If you are reading this after 2026-09-30, the assumption10// was wrong: either delete it, or promote it properly —11// tests, error types, and out of one-off/.12 13const DRY_RUN = process.env.APPLY !== 'true'14 15// resume point: it will fail halfway, and re-running must be safe16const from = Number(process.env.FROM_ID ?? 0)Four things carry the weight: the deletion date, the named blast radius, the stated verification method, and the instruction to whoever finds it later. None of it is structure, and all of it is design.
How to build it
Most important first.
- Ask how long it lives and what it can damage. Short life and small blast radius means write it directly and spend the effort on verification instead.
- Spend on the risk that exists. For a one-shot migration: a dry run, a count assertion, a transaction, a resume point, a backup. None of those are "design" and all of them matter more here.
- Make throwaway code obviously throwaway — a
scripts/one-off/directory with a date, so nobody mistakes it for a component (Repository Structure). - Notice when the assumption breaks. The second time a "one-off" is run, it is not one, and it should be either deleted or promoted deliberately.
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- For genuinely throwaway code, the cost of the next change is irrelevant, because there is no next change. That is the entire argument.
- The risk is that the premise is wrong. The expected cost is the probability it survives times the cost of it surviving unstructured — which for a prototype that ships is very high, and is why the honest move is to name the assumption rather than rely on it.
- Two standards mean judgement calls, and judgement calls can be abused. "This is throwaway" is an easy thing to say about code you do not want to test.
- Uniform standards are simpler to enforce and easier to defend, and there is a real argument that the simplicity is worth the waste (The Complexity Budget).
What can go wrong
- The prototype ships. This is the dominant failure and it is organisational, not technical — the demo works, the deadline moves, and structure is never added (Deliberate Debt).
- The one-off script becomes a monthly ritual, still with no tests, now load-bearing.
- The reverse failure: applying the exemption to code that is genuinely long-lived because it *feels* small, which is how core modules end up untested.
- Throwaway code should depend on production code and never the reverse. The moment something imports the script, it is not throwaway any more.
- "So prototypes do not need care." They need a different kind: correctness on the day, a bounded blast radius, and a clear label. That is care, aimed elsewhere.
- "Anything I call a spike is exempt." The exemption comes from the lifetime being genuinely short, and that is a prediction which can be wrong. The honest version states it out loud.
- "This contradicts the rest of the domain." It is the rest of the domain applied consistently: structure buys change locality, and where there is no change there is nothing to buy (YAGNI, With Its Bill Attached).
Testing it, and how it ages
- Not none, but different. A one-shot migration wants a dry run against real data and an assertion on row counts, not unit tests of its helper functions.
- The test that matters for throwaway code is usually "did the output match what we expected", run once, by a human (Characterization Tests).
- The only evolution question that matters is whether the code survives its intended lifetime. Everything else follows from that one fact being right or wrong.
- Teams that are good at this have an explicit way for throwaway code to be promoted — reviewed, tested and moved — rather than drifting into permanence unnoticed.
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- LIFETIME-SPECIFICThis is the entire lesson — the argument holds for code with a genuinely short life and inverts completely for code that must keep absorbing requirements. The failure mode in both directions is misjudging which one you are writing.
- CONTESTEDA strong opposing position: prototypes ship far more often than anyone predicts, so the expected cost of the exemption is much higher than it looks, and a uniform standard is the cheaper policy even though it wastes effort on genuinely disposable code. Teams that have been burned by a shipped prototype hold this view for good reason, and the disagreement is empirical rather than philosophical.
- SCALE-SPECIFICOn a solo project the author knows what is disposable; at fifty engineers nobody does, so the label has to be structural — a directory, a lint rule, a deletion date — rather than something someone remembers.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — Testing & Reliability Engineering — verification and structure are separate budgets, and one-shot high-risk work needs the first without the second.