Engineering Journal
Schema Editor
Schema Editor

A feature that opts out of your shared lifecycle will reimplement it, badly

2026-08-02

TLDR

Our measure feature had its own on/off flag instead of joining the tool system. It did not thereby avoid needing activation, cancellation and persistence. It just implemented all three worse, privately, and nobody noticed because each gap looked like a small missing feature rather than one structural mistake.

The position

Most apps grow a shared lifecycle for their main interaction: a tool system, a command registry, a job runner. Then a feature arrives that does not quite fit, and someone gives it a boolean.

That boolean is not a shortcut past the lifecycle. It is a commitment to reimplement it, one piece at a time, somewhere nobody will look.

What the industry does and why

Opting out is locally rational every time. Conforming means bending your feature or widening the system; a flag is five minutes. It also reviews well: if (this._measuring) is legible, and nobody thinks to ask "does this participate in cancellation" because the flag never raises the question.

There is usually a real objection too. Our tool system cancels any in-progress draw on tool switch, and measure had a dialog to show first.

Why it fails

Because the lifecycle is not optional, it is just unowned.

Measure, as a mode:

Read that list as bug reports and it is five unrelated papercuts. Read it as one sentence and it is: this feature is not in the tool system. When a feature accumulates several small gaps that are each cheap to fix, stop fixing them and ask what it is not participating in.

The flag caused a second failure directly. Measure owned its settings dialog and only started once you pressed OK, welding a document setting (scale, set once) to an action (measuring, repeated). You could not measure twice without re-confirming your units. Nobody designs that; it falls out of one entry point serving two concepts.

The better approach

Register it, and solve the one genuine conflict explicitly.

_DRAW_TOOLS: ['pen', 'line', 'rect', 'ellipse', 'polygon', 'text', 'wire', 'measure'],

That line delivered Escape, tool lock, auto-revert, cursor, hint and consistent activation. The real objection was answered with a guard, not an architecture:

if (tool === 'measure') this._measureBegin?.();
else                    this._measureEnd?.();

Settings moved to the toolbar, where settings live: one button opens units and calibration, another activates the tool.

The output then became a real document element rather than overlay debris, which unlocked something unplanned. Once a measurement is an element whose vertex list is its source of truth, it inherits the editing model too: node handles, drag to reshape, double-click to add one. The same interactions as a wire, because it is the same kind of object, sharing code rather than paralleling it.

That is the argument in one observation. Conforming did not cost a feature, it handed us one, because membership means inheriting the improvements too.

What you give up

Real things. Your feature inherits decisions made for other features, so if the shared system is wrong for you, conforming propagates its wrongness. Widening it touches shared code: a bigger diff and a wider blast radius than a flag.

And some features are not that kind of thing. Splitting measure into a tool plus separate settings worked precisely because half of it did not belong in the tool system either.

When the flag is right

When the thing genuinely has no lifecycle: a modal, a rendering toggle, anything with no notion of starting, being cancelled, or producing output.

The test is one question. Can this be interrupted? If a user can be halfway through it, it has a lifecycle, and you will implement one whether you meant to or not. The only choice is whether you write it once in the shared place or a fifth time in private.

Read this post in the full Engineering Journal →