deciding-reversibility

Updated

Use when a choice is blocking progress and the deliberation is costing more than the choice would, when picking a name, a library, a file layout, a schema, an interface, or a default, when a discussion has gone several rounds without new information entering it, or when an action would write data, publish an interface, delete something, overwrite a setting or a configuration file, or otherwise be expensive to undo. Covers one-way and two-way doors, cost of delay, and decision altitude. Also use when an action is being called reversible because a backup, snapshot, rollback, or kill switch exists, and nothing has checked whether that action can reach it, or when a retention rule is about to delete by age or count from a folder that holds more than one kind of file, or when a write is about to run with a credential or connection string nobody has read the target out of. Also use when artifacts are uploaded and a manifest or pointer that running clients poll is about to be published.

Overview

Match the cost of deciding to the cost of being wrong. The dominant failure is not bad decisions — it is expensive deliberation spent on choices that could be undone in ten minutes, while the genuinely irreversible ones get made in passing.

When to use

  • A choice is blocking progress and deliberating now costs more than the choice does
  • Picking a name, a library, a layout, a schema, an interface, or a default
  • A discussion has run several rounds with no new information entering it
  • About to write data, publish an interface, send something, or delete
  • Not for: generating the options in the first place — steelmanning-alternatives owns that. This owns how much process the choice deserves once options exist.

What makes a door one-way

Not importance, and not how permanent it feels. These predictors:

  • Data gets written in a shape you would later have to migrate, or data gets destroyed.
  • Consumers outside your control have copied it — published interfaces, wire and file formats, URLs, command-line flags, anything already integrated against.
  • It was said out loud — user-visible names, external commitments, anything announced.
  • Trust or secrecy is spent — leaked credentials, rewritten history, messages sent.
  • Other work will compound on it before anyone revisits it.

Two-way despite feeling permanent: internal module boundaries, most naming, directory layout, any dependency reachable only through your own wrapper, algorithm choice behind a stable interface, defaults with no data written against them yet.

The rule that catches most misclassification: reversibility is the cost to undo after the work built on top of it, not the cost to undo today. A choice that is trivial to change now and gets built on for two weeks was a one-way door the day it was made. The clock closes doors, not the choice.

The door is wherever the credential opens, not wherever the task said. A command's reach is decided by the credential and the connection string it runs with, and the task's description of the environment — "the staging bug", "the local database", "a scratch copy" — is only a belief about them. Before any write, delete, or migration, read the target back out of what will actually be used: the host in the connection string, the project or account the token belongs to, the cluster the current context names — read from the configuration itself, not by connecting through the application, since opening the store can be the write (below). Two failures in 2026 had the same shape: an agent fixing a staging problem used an API token it found in an unrelated file, and deleted a production volume along with the backups stored on it; a migration-planning command, pointed through a secondary setting at the production database, emptied every table there. A credential the work found rather than was given is the plainest sign — its reach is whatever its owner could do, which the task never scoped — and using one is a question for a human, not a convenience (bounding-autonomous-work).

Looking can be the write. Classify the act of acquiring the thing, not only what you meant to do with it. Opening a datastore can run its schema migrations; attaching to a service can take a lease or replay a journal; mounting or loading a file can rewrite a lock or an index. A "read-only" preview, dry run, or inspection therefore may already have changed the thing it exists to inspect, and a safety copy taken after the handle is open preserves the state the open already altered. Copy before anything opens it, on every path including the read-only one.

Matching process to weight

Cost to undoBudgetForm
Minutes — rename, reorder, swap a local implementationDecide immediately, alonePick; note the runner-up in one clause
Hours to a day — move a boundary, change an internal shape before data existsOne bounded comparison of two optionsPick; state the deciding criterion in a line
Days to weeks — a data shape with rows in it, an interface another team consumesWrite the options and the criteria; involve the ownerA recorded decision
Effectively permanent — public API, destructive migration, deletion, external sendDo not decide unilaterallySurface it and wait for an explicit answer

Calibration: if undoing costs under roughly an hour and no data or external consumer is involved, deciding should cost under five minutes. Three rounds of discussion about a ten-minute change has already cost more than the mistake would have.

Delay against wrongness

Both terms are computable to within an order of magnitude, which is all that is required.

  • Cost of delay = what is blocked, times how long. Dependent work queued behind a decision makes deliberation superlinearly expensive, and this is the term that gets forgotten.
  • Cost of being wrong = undo cost, times probability of being wrong. A likely-but-cheap error is usually a better trade than an unlikely-but-expensive one.

The stopping predicate is not whether you feel sure. It is: if another round of analysis would not change which option you pick, the analysis is finished. Name the evidence that would change the answer; if you cannot name any, or it is not obtainable now, decide.

Convert one-way doors into two-way ones

Usually higher leverage than deciding well — change the decision's class so it can be made in five minutes instead of five days.

  • Put the choice behind an interface so the implementation can be swapped later.
  • Version the format on day one so a second version is legal without a migration.
  • Confirm the migration path exists before committing to a shape, rather than after.
  • Gate it so rollback is a configuration change rather than a deployment.
  • Keep the old path alive until the new one has carried real traffic.
  • Deprecate rather than delete; append rather than overwrite.
  • Read a setting before you write it, and keep what you read — the prior value is what makes the write undoable, and it is gone the instant the write lands.

The last of those is the one that gets skipped, and it gets skipped for a reason that sounds like caution. A configuration write is a one-way door only because the value it replaces is destroyed by the write, so the prior value has to be captured in the same operation — a separate "read it first" step is the step that never runs. That holds when the new value is believed to be strictly safer, and that belief is exactly what skips it: twice in one session a "safe" value was written over one that was already more permissive, a silent downgrade of somebody's environment made while trying to help, and a third write clobbered a tracked configuration file instead of merging into it. Nothing about the new value being safe makes the old one recoverable.

The wrapper generally costs less than the analysis it removes. When it does not, that is the signal the decision genuinely deserves the analysis.

And the undo has to live outside the blast radius of the thing it undoes. Every item above assumes the mechanism that reopens the door still exists once you have gone through it, and that assumption is the one nobody states. Backups stored in the volume they protect, under the credential that can delete that volume, are not a rollback plan — they are part of the same object, and one action removes the door and the way back through it together. The shape recurs far from storage: a kill switch served by the service it kills — feature-flagging owns that one, and requires an evaluation path that does not depend on anything failing during the incident the switch was built for — a rollback that needs the pipeline the bad deploy just broke, a revert to an artifact the registry garbage-collected last week, a runbook stored only in the wiki that runs on the cluster being drained.

The check is one question at design time, and it is cheap enough to ask about every reversible-looking action: name the thing that would perform the undo, then ask whether the action you are contemplating can reach it. Different volume, different credential, different account, different lifecycle — any one of those separations is usually enough, and none of them is the default.

A rotation is the same hazard wearing a schedule. "Keep the newest N" and "delete anything older than D" are rules that name a place and a clock, and they cannot tell a rolling copy from a record. Put one deliberate archive in the folder the rule sweeps — the pre-migration snapshot, the last known-good build, the copy kept because something is about to change — and it is the oldest file there by construction, so the rule takes it first, on schedule, with no error. Another writer filling the folder past the limit is enough to defer the pruning to a later run that then prunes harder than anyone expected, and the loss is recoverable only by luck. Two moves close it: have the rule name what it rotates by a pattern that only the rotating kind carries, never by the directory, and keep anything that must survive where no rotation reaches. And before any rule is armed that deletes by age or count, list the oldest file it would take on its next run and say why that one is allowed to go.

This is a different question from whether the undo works, and passing one says nothing about the other: confirming-before-claiming-done requires a restore actually performed from the backup, and a backup can be verified restorable and still sit inside the blast radius of the thing it protects. Separation is about identity as much as location — if one credential opens both the store and its backup, both are in the radius regardless of where the bytes sit.

Verify before a cheap switch that consumers act on, not after it

Rolling a pointer back is a two-way door only for the consumers that have not fetched it yet. A release uploads its artifacts somewhere inert, then one cheap write — a manifest, a pointer, a "latest" tag — makes them what every consumer fetches. The upload is harmless until something names it. The pointer write is the door, and its cheapness is what lets it pass as a formality. Consumers poll that pointer independently, so with a fleet of any size the first fetch lands within seconds of the write whatever the polling interval is, and a check started after the write cannot be sure to finish before it. Verification placed there can only describe the damage, and rolling back does not recall what the first consumers already fetched and ran. The same shape holds for a DNS record, a routing weight, a symlink swap, or any other single write that a population reads on its own clock.

So the order is upload, verify what a consumer would get, then flip. Fetch each artifact from the public address the pointer will name, not the path you wrote it to, hash the bytes you received yourself, and compare that with the digest the pointer is about to carry, not with a hash the server reports. It costs seconds and catches the failures an upload actually has: a truncated or half-replicated object, a stale copy served from a cache under the new name, a digest computed over a different file than the one uploaded. Make the flip conditional on the comparison in the same script, so one mismatch means the pointer is never written and a bad upload cannot roll out at all, where the same finding after the flip is an incident. What is left to check afterwards is only that the pointer is being served and says what you wrote, which confirms the flip and nothing about the contents.

The question that places the check, for any activation step: how long until the first consumer acts on it, and how long does the check take? If the first is shorter, after is too late, and since a check that takes seconds costs nothing in front, in front is the default. automating-repetition's preflight covers what can be validated before the first write; this is the check that cannot exist until the upload has happened and still has to come before the step that makes it live. One form of the mistake is a recipe whose verification step is last because it was written to confirm the release, and confirming is not preventing.

Decide at the right altitude

The two errors are symmetric. Imposing a project-wide convention to settle one file turns a local choice into a global one and manufactures a default nobody validated. Re-deciding the same thing at twenty call sites produces twenty answers and no convention.

Predicate: if everyone facing this choice would make it identically, it belongs in one place. If the right answer varies with local context, lifting it upward creates a bad default and a stream of overrides.

Pick and move, or refuse to

"Just pick one" is correct when the choice is reversible, writes no data, is invisible outside the code, and the options sit within noise of each other on every criterion that was named as mattering.

It is negligence when the choice writes data, is user-visible, sets a security or privacy default, is the thing you were specifically asked to think about, or the options differ by an order of magnitude on a criterion someone named. There, the fast move is not to pick — it is to surface the choice with a recommendation.

Record every fast decision in one line: the choice, the runner-up, and the observable that should trigger revisiting it. A decision with a named revisit trigger is one you are entitled to make quickly. When that trigger fires later and the recorded decision is now blocking work, revalidating-decisions picks up from there — separating what was actually decided from the premise it was decided under.

Common mistakes

SymptomReal cause
Extended analysis of something changeable in ten minutesWeighted by how important it feels rather than by undo cost
Decided fast, and it wrote dataConfused "easy to change the code" with "easy to change the consequences"
The same question re-decided in every fileNever lifted to the altitude where it gets made once
Asked the user to choose between options they have no stake inEscalated a two-way door; the interruption costs more than the error would
Waiting on information that is not comingNo named evidence that would change the answer
A once-reversible choice is now load-bearingWeeks of work compounded on it while it was still provisional
Both options built to avoid choosingTwo implementations to maintain, and the decision still pending
A fix aimed at a test environment changed productionThe target was taken from the task's description, not read back out of the credential or connection string the command ran with
A "keep the newest N" job deleted the one archive kept on purposeThe rule swept a folder instead of a kind of file; the archive was the oldest file in it, so it went first
The rollback plan was gone at the moment it was neededThe undo lived inside the blast radius — same volume, same credential, or same lifecycle as the thing it was meant to undo
A bad upload reached running clients before anyone noticedThe artifacts were checked after the pointer was published, when consumers polling it had already acted

Red flags

  • "It's reversible, we have backups." — said without naming where they live and what can reach them.
  • A write about to run on a token or connection string found in a file, rather than one issued for this task
  • A release recipe whose "verify" step comes after the pointer clients poll has been published
  • "Let us make sure we get this right the first time" about something internal and wrapped
  • A decision thread whose last two rounds introduced no new information
  • Reaching for a migration, a deletion, or an external send at the same speed as an internal rename
  • Choosing the harder-to-reverse option because it is marginally more elegant
  • Treating a choice as settled because implementation already started on it

More in Judgment and meta