Migration cost lands on consumers; pace is set by call-site count
Aliases: migration tax · blast radius · call-site count
What it is
A breaking change may be a few dozen lines inside the library. The bill is paid at every call site: every tag in product code, every prop, every instance in a design file. Call-site migration cost lands on the consumer, not on the team that cut the release. Pace is therefore not “can we ship this week”; it is “how many places outside must be edited by hand”. A thousand call sites are a thousand open-file, understand-the-diff, edit-and-verify jobs. There is one component; that asymmetry is the shape of the cost.
Team count and repo count are the wrong denominators. One team can hide a thousand calls behind a shell, or scatter a thousand across twenty repos. Count call sites, and the hours the consumer will pay become visible.
Why it happens
The library author’s marginal cost of changing an interface is roughly one refactor. The consumer’s cost is call-site count times time-to-understand at each point. That time is not constant: a rename is close to find-and-replace; a flipped default needs the screen looked at again; a slot restructure needs the subtree rewritten. Under the same “major” digit, actual hours can differ by two orders of magnitude, so pace cannot follow the version number alone — it follows the distribution of touched call sites.
The publisher feeling “we already signalled” does not shorten the consumer’s calendar. Products have freeze windows, release trains, and people. A thousand calls stuffed into one iteration crowd out features; spread across three, the library’s new capability is unavailable to unmigrated points for that stretch. Who sets the pace is who pays the hours — and the hours belong to the owners of the call sites.
Where it stops holding
Mechanical renames an automatic rewrite can cover no longer convert call-site count into human hours; pace can follow the tool. Semantic changes the tool cannot cover (defaults, focus order) still count per site. When there are a handful of internal calls and author and consumer are the same person, cost returns to the publisher and the asymmetry pauses. Generated code and compile-time macros hide call sites in the build graph: few handwritten, explosion in the generated artefact — the denominator must move to post-generation nodes. When the system is treated as one-off scaffolding and call sites were never going to follow versions, there is no object for pace — that is abandoning the library, not migrating slowly.
Applying it
- Before a breaking change, query call sites: counts by repo, by component, by kind of edit (rename / default / structure). Cap pace with an hour budget, not the publisher’s weekly calendar.
- Put the call-site estimate on the change record next to the breakage digit; a record without an estimate is incomplete.
- Prefer breaks with few call sites or mechanical-rewrite coverage. For dense sites that need human understanding, split into several smaller digit moves rather than one bundle.
- How to check: on a finished major, search or compile-count the call sites actually edited, and compare to the pre-release estimate. More than 2× off, do not schedule the next break without a new count. Then ask owners of three product repos how many iterations the follow took — if the publisher’s log says “caught up in a week” and call-site owners say “still in iteration three”, pace is still not being set by the people who pay.