What compatibility promise does a suite's extension point make to the code that plugs into it?
answer
- It is a contract in both directions
- Shape, timing, multiplicity, failure policy
- A positional list cannot grow
- Blast radius is edits times dependants
basics
~20 sPublishing an extension point freezes what the framework passes in, what it accepts back, when it calls and how often, and what a thrown error does. Widening any of those forces every dependant to change at once.
solid answer
~50 sAn extension point is a contract in both directions plus guarantees that never appear in the signature. Inbound, it promises the shape of what is handed to the implementation. Outbound, it promises what may be returned and what the framework does with it. It also promises **timing and multiplicity** — when the call happens and how many times — and a **failure policy** for a thrown error. Adding a positional parameter breaks every implementation at once, because each must be edited before it will load. Passing a single **record** the framework can grow, whose new fields older implementations simply ignore, turns the same change into an additive one. Note that most breaking changes are invisible to a compiler: moving the call, calling it twice, or making a swallowed error fatal all change behaviour everywhere while compiling fine.
code
pseudocode · 15 lines# breaking: every implementation must change in lockstep
on_case_end(case_name, outcome)
on_case_end(case_name, outcome, attempt_index) # added parameter
# additive: older implementations keep loading untouched
record CaseEnded:
case_name
outcome
attempt_index # new field; implementations may ignore it
on_case_end(event: CaseEnded)
# terms that are not in the signature, so write them down
# called once per attempt, after assertions, before cleanup
# a thrown error is recorded against the extension, run continuesgo deeper
Understand that once other code plugs into a seam, its shape stops being yours to change freely. Be able to say what an operation's signature is and why other people's code depends on it staying put.
Explain the difference between an additive change and a breaking one at a seam, and why handing over a single growable record rather than a list of parameters decides which category most future changes fall into.
Show that you count the promise beyond the signature: when the call happens, how often, what a thrown error does, and what a returned value means. Be ready to name a change that compiles everywhere and still breaks every dependant.
Own how narrow a seam the organisation publishes and what it commits to in writing, including what is deliberately left unpromised. The judgement is exposing enough for teams to build what they need against keeping a surface you can still honour years later.
## The promise is wider than the signature Publishing an extension point is publishing a contract in **both directions**, plus a set of guarantees that never appear in the signature at all. Four things get frozen the moment somebody implements it: - **Inbound shape.** What the framework hands the extension: which values, of what shape, meaning what. Anything reachable from what you pass is also part of the promise, whether you meant it or not. - **Outbound shape.** What the extension may hand back and what the framework does with it. A return value that the framework silently ignores is a promise you did not intend to make; a return value whose meaning you later reinterpret breaks every implementation without touching a signature. - **Timing and multiplicity.** When the call happens relative to the rest of the case, and how many times. Once per case, once per attempt, before or after cleanup — these are contract terms. - **Failure policy.** What a thrown error does. Isolated and recorded, or fatal to the case? Implementations are written to that answer, so changing it changes behaviour everywhere. A useful way to hear the promise is to ask what an implementer had to assume in order to write their code. Everything they assumed and got right is something you are now committed to. ## Additive versus breaking The distinction that matters day to day is whether existing implementations keep working untouched. | Change | Category | Why | | --- | --- | --- | | Add a field to a passed record | additive | implementations that ignore it are unaffected | | Add a parameter to the called operation | breaking | every implementation must be edited before it will load | | Add a new optional operation with a default | additive | existing implementations inherit the default | | Change what a returned value means | breaking | implementations keep returning it, meaning something else | | Move the call earlier in the case | breaking | the same code now runs against a different point in the flow | | Call once per attempt instead of once per case | breaking | anything counting or accumulating now double-counts | | Make a previously swallowed error fail the case | breaking | implementations relied on isolation and now stop runs | Only two rows on that table are visible to a compiler or loader. The rest compile everywhere and change behaviour silently, which is why **the signature is a poor proxy for the promise**. The shape you choose at publication decides which column most future changes land in: 1. **Pass one record, not a parameter list.** A record can grow a field; a positional parameter list cannot grow anything without an edit at every implementation. This single choice converts the most common kind of change — *the extension now needs to know one more thing* — from breaking into additive. 2. **Return something narrow and declared.** A small verdict the framework interprets, not an internal object. If an extension can hand back your internals, your internals are part of the contract. 3. **Write the timing and failure policy down beside the contract.** They are terms, not implementation details, and they are the ones that get broken by accident. 4. **Publish the least you can defend.** Every field exposed is a commitment. Data you did not publish can always be added; data you published can never quietly go away. ## Why the blast radius is the number that matters The cost of a breaking change is not the size of the edit. It is the size of the edit multiplied by the number of implementations, and — the part people miss — those implementations are not all in front of you. A seam that a dozen suites plug into, owned by teams you do not sit with, turns a one-line signature change into a coordination problem across a dozen calendars. Nothing about the change is technically hard; all of the cost is in the count and the coordination. That is the real argument for keeping a seam narrow. A contract with three fields and one operation is a small thing to keep honouring. A contract that grew to twenty fields, half of them exposing internals somebody reached for once, is a permanent tax: every internal refactor now has to check whether it changed something a stranger's implementation reads. Two habits keep the promise keepable: - **Have the framework test its own contract** against a reference implementation that asserts the timing, the multiplicity and the failure policy. Then a change to any of them fails your build rather than a stranger's run. - **Say what is not promised.** Explicitly documenting that ordering between independent extensions is unspecified, or that an extension may be called on a pooled worker thread, prevents an assumption from hardening into a term you never agreed to. What you refuse to promise now is the freedom you keep later.
- Which parts of the promise are easiest to break by accident?Timing and multiplicity, because neither is in the signature. Moving the call from after the assertions to after cleanup, or invoking it once per attempt where it used to be once per case, compiles everywhere and changes what every implementation observes. If those are terms, they belong in the written contract and in a check the framework runs against a reference implementation.
- How do you keep the promise small when you first publish a seam?Publish the least you can defend. Hand over a record carrying only the fields two real dependants actually needed, accept back a narrow declared verdict rather than letting an implementation return your internals, and leave anything speculative out. Every exposed field is a commitment you must keep honouring, while data you never published can always be added later without breaking anyone.
saying these in an interview costs you the question
- Thinks the signature is the whole contract
- Adds a required parameter and calls it minor
- Exposes internal framework objects through the seam
- Leaves calling order and timing undocumented
- Assumes every dependant lives in one repository