Sticky Contract
Context
Section titled “Context”You extracted a unit from a caller for legibility or testability. Over time, the unit’s signature, behavior, and side-effect shape become a contract that constrains future change — even though no consumer outside the original caller actually depends on the contract being stable.
Cost type
Section titled “Cost type”Primarily cognitive: future changes to the unit feel risky, code review pushes back on signature changes, the unit’s shape is preserved out of caution rather than necessity. The cost shows up as drag on the original caller’s evolution.
What goes wrong
Section titled “What goes wrong”- The original caller could absorb a refactor that changes the unit’s shape, but the team treats the unit as load-bearing and does the change in a slower, more conservative way.
- Tests at the unit boundary are preserved as if they were a published contract; new behavior is added by extending the contract rather than by reshaping it, accumulating cruft.
- The unit becomes the kind of code people work around rather than work with. Multiple slightly-different helper extractions appear because nobody wants to disturb the original.
- New consumers of the same logic end up duplicating it, because the existing unit’s shape doesn’t quite fit and changing it feels too risky.
Prospective indicators
Section titled “Prospective indicators”- You’re hesitating to change a unit’s signature even though only one caller exists.
- A code review comment asks “are you sure this is backwards-compatible?” about a unit with one consumer in the same file.
- New behavior is being shoehorned into the existing unit’s shape rather than reshaping the unit.
Retrospective indicators
Section titled “Retrospective indicators”- Units with one or two consumers but stable signatures over years, while the consumers themselves churn.
- “Helper” or “utility” extractions of the same logic appearing in multiple places because the original was too rigid.
- Changes to the original caller that touch lots of files for no clear reason — the unit’s contract is leaking constraint outward.
Prevention
Section titled “Prevention”- Treat extracted units inside a feature as implementation details until proven otherwise. Mark them as such (folder placement, naming, optionally a comment).
- When changing the original caller, give yourself permission to also reshape its helpers in the same commit. Don’t treat extraction as a one-way door.
- Don’t preserve unit-level tests when the unit’s shape is changing — rewrite or delete them. Tests are scaffolding, not contract.
- If you find yourself preserving a contract for “future consumers that might exist,” ask whether those consumers actually exist. Almost always: they don’t.
When this isn’t a failure mode
Section titled “When this isn’t a failure mode”- The unit has multiple real consumers and a documented, owned contract. Stability is the value.
- Intrinsic-contract code (Luhn check, parsers, etc.). The contract is the spec; preserve it.
- Library code being prepared for publication. Stability of the public surface is exactly the deliverable.