Revisit Triggers
A decision that was right becomes wrong when its assumptions change, and nobody is watching the assumptions. Naming the evidence that would flip the answer is the cheapest engineering practice there is, and the least used.
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.
This design is correct today. What specific, observable thing would tell us it has stopped being correct — and who would see it?
Sessions are held in an in-memory map because there is one application process. It has been right for two years. There are now three processes behind a load balancer and users are being logged out at random.
We will notice when it becomes a problem. Good engineers can tell when something has outgrown its design.
You notice when it becomes an *incident*, which is later and much more expensive than when it became wrong. Random logouts are the symptom; the decision stopped being correct the day a second process was planned.
- You notice when it becomes an *incident*, which is later and much more expensive than when it became wrong. Random logouts are the symptom; the decision stopped being correct the day a second process was planned.
- The people who would notice are not the people who know the assumption. Whoever added the second process was solving a capacity problem and had no reason to know that session storage assumed one process (The Requirements Nobody States).
- Assumptions decay silently and in the background. Nothing emits an event when your traffic crosses the level where polling stops being sensible, or when the team grows past the size where a shared module was cheap to coordinate.
- And the decision has by now acquired defenders. Two years of code depends on the in-memory map, so the conversation is no longer "is this right" but "who is going to pay for changing it" (Stability and Dependency Direction).
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.
- Nobody is going to read a document quarterly to check whether old decisions still hold; that practice has been tried and abandoned twice.
- The signals that matter — traffic, process count, team size, data volume — are mostly already on dashboards nobody connects to design decisions.
- The engineer who made the original decision has left.
- A decision with a fired trigger is either re-decided or explicitly re-affirmed. Silently continuing is the failure this practice exists to prevent.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- The person making the decision owns naming the trigger, at the moment of deciding, when the assumption is still conscious.
- The trigger owns being observable by someone who does not know the decision — a metric, an alert, a lint rule, a checklist item on a related change.
- Whoever the trigger fires at owns re-deciding or re-affirming in writing. Both outcomes are fine; silence is not.
- A trigger belongs with the decision record, and a copy of it belongs wherever it can actually fire — a dashboard alert, a comment at the top of the module, a check in the pull request template (Docs Close to Code).
- The boundary between a good trigger and a bad one is observability by a stranger. "If this becomes a problem" requires the reader to already hold the assumption; "if a second process is added" does not.
What a trigger has to look like to ever fire
Most written triggers fail on the same property: they can only be recognised by someone who already knows the assumption. The person who will actually encounter the trigger is a stranger, doing something else, two years later.
The repair is to phrase the trigger as an event or a number that a stranger would notice while doing their own work — adding a process, crossing a dashboard threshold, receiving a particular kind of request.
1In-memory session map2 weak Revisit if session handling becomes a problem.3 strong Revisit when the deployment runs more than one process4 (checklist item in deploy/README) OR when sticky sessions5 are configured on the load balancer.6 7Polling worker every 15s8 weak Revisit at higher volume.9 strong Alert at 4k scheduled runs/day (reports.scheduled_runs_daily,10 threshold set at 80% of the 5k where polling gets silly).11 12One shared pricing module for all product lines13 weak Revisit if pricing gets complicated.14 strong Revisit when a second product line needs a rule the first15 one must not have — i.e. the first time a caller wants a16 flag on a pricing function.17 18No caching layer19 weak Add caching if slow.20 strong Revisit when p95 on /reports exceeds 800ms for a week, OR21 when the same query appears in two request paths.The strong versions share one property: each names something a stranger would trip over while doing unrelated work. The pricing one is the subtlest and the most useful — "someone wants a flag on this function" is a structural event that shows up in a pull request, which is exactly where the assumption needs to be reconsidered (Boolean Parameters).
The lifecycle a decision actually has
Decisions are usually treated as having two states, made and not made. They have more, and the interesting ones are what happens after the trigger fires — because a fired trigger with no response is the state where all the damage happens.
Making the lifecycle explicit gives the practice something to enforce. A trigger that has fired and been neither acted on nor re-affirmed is a visible, nameable condition rather than an absence nobody can see.
| From | On | To | Guard | Effect |
|---|---|---|---|---|
| Proposed | decision made | Active | a record exists with a named trigger | the trigger is registered somewhere it can fire — alert, checklist, comment |
| Active | the named evidence appears | Triggered | someone receives the signal | the decision is put back on an agenda |
| Triggered | reconsidered and kept | Re-affirmed | a new trigger is written | the record gains a dated note saying why it survived |
| Triggered | reconsidered and changed | Superseded | the new record links to this one | migration planned as its own piece of work (Designing the Migration) |
| Active | the record is lost, or was never written | Folklore | — | the decision becomes unexaminable and acquires defenders |
- Triggered → Active — A fired trigger cannot be un-fired by ignoring it. Going quietly back to Active is the exact failure this practice exists to prevent: the assumption is now known to be violated and the design is still built on it. The only legal exits are Re-affirmed, with a new trigger and a dated reason, or Superseded.
- Superseded → Active — Reviving a superseded decision without a new record erases the evidence that it was tried and abandoned, and teams that do this go round the same loop every eighteen months (Decision Records).
- Proposed → Folklore — A decision that was never explicitly made but is now load-bearing — the default nobody chose. It is the most common origin of folklore and the reason "we always do it this way" needs the question "who decided that?" asked out loud (Knowledge Sharing).
Re-affirmed is the state teams forget exists. Deciding to keep the in-memory map after a second process is planned is a perfectly good outcome — as long as it is a decision with a new trigger, rather than an omission.
How triggers fail in practice
The practice is one sentence long and still fails routinely, in ways that are worth recognising because each has a cheap structural fix.
The pattern across all of them: a trigger is a message to a stranger in the future, and messages fail on delivery far more often than on content.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Threshold crossed on a dashboard | Nothing happens; the graph is above the line for four months | The trigger fired at a chart rather than at a person or a channel | Attach triggers to alerts with an owner, or to checklists in work that is already happening (A Review Checklist Worth Reading). |
| A second process is added for capacity | Random logouts in production two weeks later | The trigger existed in a decision record the capacity engineer had no reason to read | Put a copy of the structural trigger where the change happens — a comment at the top of the session module and a line in the deploy checklist (Docs Close to Code). |
| Trigger fires during a launch freeze | Team agrees to look at it "after the launch"; nobody does | Re-affirming was never made a legitimate, recordable outcome | Allow explicit re-affirmation with a new trigger and a date. A recorded deferral is durable; a verbal one is not. |
| The threshold turns out to be wrong | A debate about the number replaces the debate about the design | The trigger was written as a number without the assumption behind it | Always state the assumption next to the threshold — "assumes polling, which stops being sensible around 5k/day" — so a wrong number does not invalidate the point (Decision Records). |
How to build it
Most important first.
- Write the trigger as a fact someone else could notice. A number with a dashboard, a structural event ("a second X appears"), or a requirement type ("anyone asks for Y").
- Prefer triggers attached to things that already happen. A line in the deployment checklist about process count fires reliably; a calendar reminder to review architecture does not.
- Name the assumption explicitly, not just the threshold. "This assumes one process" is what lets someone recognise the trigger from a different direction than you imagined (Decision Records).
- Give each trigger a place to fire *at* — a team, a channel, an alert. A trigger nobody receives is a sentence in a document.
- Where a trigger is cheap to automate, automate it. An alert at eighty percent of the threshold is a revisit trigger that does not depend on anyone remembering (Stable Identifiers and other design-for-observability moves make this possible).
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.
- Trigger fired early, design changed then: moving sessions to a shared store while there is one process is a contained change — one module, one deployment, no user impact.
- Trigger missed, discovered by incident: the same change is made during an outage, by whoever is on call, with two years of accumulated coupling to the in-memory map and no time to characterize what depended on it.
- The practice costs one sentence per decision. It is the highest-leverage sentence in this module, and it is omitted from the large majority of decision records that get written.
- Writing triggers invites the illusion of coverage: the risks you thought of are named, and the assumption that breaks you is usually the one nobody articulated.
- Automated triggers add alerting surface, and a trigger that fires at eighty percent of a threshold that then plateaus becomes noise nobody acts on.
- Naming a trigger commits you to acting when it fires, which is occasionally inconvenient — teams have been known to argue the threshold was wrong rather than reopen the decision.
What can go wrong
- The trigger fires and nothing happens, because it fired at a dashboard rather than at a person. This is the most common outcome of automated triggers.
- Triggers are written vaguely — "revisit if this becomes a bottleneck" — which is unfalsifiable and therefore never fires.
- Everything gets a trigger, the list becomes noise, and the three that mattered are lost among forty that were written to satisfy the template (The Complexity Budget).
- The trigger fires, the team re-decides, and nobody records the re-decision, so the next person finds a fired trigger and an unchanged design and cannot tell whether it was considered.
- Triggers depend on measurement existing. A threshold on a number nobody records is aspirational (Debuggability by Design).
- They depend on the decision record existing, or there is nothing for the trigger to point at.
- Downstream, everything built on the decision depends on it silently — which is what makes late discovery so expensive (Change Amplification).
- "This is just a TODO." A TODO says work is outstanding. A trigger says the current state is correct *until* a specific thing happens, which is a different and much more useful claim (Deliberate Debt).
- "We review architecture quarterly, so we do not need these." Quarterly review covers what is on the agenda. Triggers cover the decision nobody remembered to put on it.
- "Every decision needs a trigger." Decisions that are cheap to reverse do not — the trigger is that you noticed (Reversible and Irreversible Decisions).
- "Setting a trigger means we plan to change it." It usually means the opposite: the decision is right now, and this is how we will know when it stops being (The Cost of Change).
Testing it, and how it ages
- Test that the trigger can fire: if it names a metric, check the metric exists and has data. A surprising number of triggers reference numbers nobody collects.
- For structural triggers, a check in CI is often possible — a lint rule that fails when a second process type appears in the deployment config, or when a forbidden import shows up (What to Automate Out of Review).
- The set of live triggers is a good description of what a team believes about its own future, and reading it once a year is more useful than any architecture review.
- Triggers should be retired when their decision is superseded, or the list accumulates dead entries and stops being read.
- As a system matures, structural triggers ("a second process", "a second tenant") fire earlier and matter more than numeric ones, because the structural assumptions are the expensive ones to unwind (Reversible and Irreversible Decisions).
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.
- GENERALThe idea that a decision is valid relative to assumptions holds everywhere; what changes by context is which assumptions are volatile — process count and data volume in a service, team size and module ownership in a library, regulatory scope in anything financial.
- LIFETIME-SPECIFICFor code with a short life, triggers are waste: nothing will change before it is deleted. Their value scales with how long the decision has to survive and how many people will pass through it, which is why they matter most in exactly the systems that are least likely to have them.
- CONTESTEDThe strongest opposing view is that triggers are a comforting fiction: the assumptions that break systems are the ones nobody thought to name, thresholds get argued away when they fire because there is always a deadline, and teams that write them still discover their problems through incidents. On this account the real answer is designing so that being wrong is cheap — reversibility over foresight. That critique is largely correct about numeric thresholds; it is weaker against structural triggers, which fire on events a stranger can recognise without holding the assumption.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — Testing & Reliability Engineering — a revisit trigger and an SLO are the same mechanism applied to different objects: a named threshold, an owner, and an obligation to act rather than to observe.