skip to content

A team moved a per-call helper's placeholder up onto the connection declaration - why must callers now hold several connections?

level: seniorimportance: should knowfreq 44%

answer

  1. commitment moves from call to construction
  2. count instances a caller must hold
  3. most members never mention it
  4. shared state splits per type
  5. widening to a universal type is worse

basics

~20 s

A placeholder on the declaration is fixed when the instance is built, so one instance can serve only one type. Work that used to pick its type per call now picks it per object, and a caller handling several types needs one connection for each.

solid answer

~40 s

Hoisting moves the commitment from the call to the construction. Before, the helper chose its type at each call and one connection served every type the caller had; after, the connection itself is built *for* a type, and the helper can only be used at that type. A caller who encodes three kinds of message now opens, configures and tears down three connections - three sets of state, three lifecycles, three things to keep in sync - to get work that was never about the connection's own message type. The tell is that most members never mention the hoisted name, and callers construct instances per type rather than reusing one.

go deeper

for a junior

Hold on to the consequence: once a placeholder sits on the type, each object is built for one type, so handling several types means several objects.

for a middle

Explain where the commitment moved and what it drags with it - setup, teardown, buffers and per-type state that no longer sees all traffic.

for a senior

Diagnose from the signature: count the members that mention the name, count the instances a caller with n types must hold, and argue the repair as the breaking change it is.

for a principal

Own the rule for the codebase: declaration sites are published commitments, so decide them at interface design time and treat a late move as a coordinated change across every consumer.

## What hoisting actually changes Moving a placeholder from an operation's signature up to the declaration looks like tidying: one name instead of several, less noise on each member. What it really does is **move the moment of commitment**. An operation-level name is chosen when the call happens; a declaration-level name is chosen when the object is built. Everything else follows from that one shift. In the chat-client setting: a connection exposes an encode-and-queue helper, originally written with its own placeholder so any value that has an encoder can be queued. Hoisted onto the connection, the helper's name becomes the connection's name, so the connection is now a *connection for one kind of value*. ## The bill the caller pays 1. **One instance per type.** A caller with three message kinds needs three connections, because an instance fixes the name once. The object's identity has been tied to a type it never needed. 2. **Lifecycle multiplied.** Connections are not free objects: each has setup, teardown, buffers, retry state. Three of them means three of each, and three things to keep consistent. 3. **Shared state is split.** Anything the connection accumulated for all traffic - sequence numbers, a backlog, statistics - now lives separately in each instance and no longer sees the whole picture. 4. **Passing it around gets harder.** Code that used to accept *a connection* now has to name the type too, and that requirement spreads outward through every signature that holds one. 5. **Callers who never use the helper still pay.** They must supply a type argument at construction for a member they never touch. ## Reading the smell in a signature The diagnostic is a counting exercise, not an opinion: | Evidence | Reading | |---|---| | Most members never mention the name | It does not describe the object | | No stored state is typed by it | Nothing has to survive between calls | | Callers build one instance per type | The commitment landed in the wrong place | | One operation uses it, once, in its own signature | It was per-call work all along | The inverse smell exists too, and it is worth naming so the rule does not get applied blindly: if every member mentions the name, stored state is typed by it, and callers reuse one instance across many calls, then the declaration is exactly where it belongs and pushing it down would destroy the agreement between members. ## Undoing it The repair is to give the operation its placeholder back and leave the declaration parameterized only by what it genuinely stores - or, where the connection stores nothing of the kind, not parameterized at all. Two things make this harder than it sounds: - **It is a breaking change at every call site**, because construction stops taking a type argument and the operation starts choosing one. There is no way to do it gradually within one signature. - **It can expose a second, real problem**: sometimes the hoist happened because something *was* being stored per type, and pushing the name down forces that state to be typed honestly instead. ## The judgment to demonstrate The interviewer is checking whether you read a declaration site as an **API commitment** rather than as syntax. The question to ask out loud is: *how many instances will a caller with n types end up holding?* If the answer is n and the object is expensive, the placeholder is in the wrong place. If the answer is one because every call at that instance genuinely is about the same type, it is in the right one. A related trap is to fix the instance explosion by widening the hoisted name to some universal type and narrowing inside the operations. That restores the single instance and throws away the checking that motivated parameterizing in the first place - a strictly worse position than either honest choice, and the thing to argue against in review. Languages differ in how much ceremony each site costs to write, which is part of why the wrong hoist happens: the tidier-looking declaration is sometimes the more expensive contract. The scoping consequence, though, is the same everywhere - a name on a declaration lives as long as an instance, so it prices every instance the caller has to make.

  • What would change your mind and make the hoist correct?
    Evidence that something must survive between calls at that type: stored state typed by the name, or two operations that have to agree about it. Then one instance per type is not an accident of the declaration site, it is the contract, and callers holding several connections is the honest cost of it.
  • Why is pushing the placeholder back down not a gradual migration?
    Construction stops accepting a type argument at the same moment the operation starts choosing one, so both ends of every call site move together. There is no intermediate signature that satisfies old and new callers, which is why the site is worth getting right when the interface is first published.

saying these in an interview costs you the question

  • Says hoisting a placeholder is a purely cosmetic tidy-up.
  • Fixes the instance explosion by widening to a universal type and narrowing inside.
  • Believes a declaration-level name can still be re-chosen per call.
  • Thinks callers who never use the operation avoid the construction commitment.
  • Treats any declaration-level placeholder as a smell, including ones members share.