When does adding a third placeholder to a registry declaration signal that the type is doing two jobs?
answer
- the count is not the criterion
- do the placeholders partition
- members against placeholders, marked
- a caller naming what it never uses
- split along the partition
basics
~20 sWhen the placeholders partition: no member mentions all of them, and different call sites care about different subsets. That means two responsibilities were glued into one declaration, and the fix is to split the type rather than to keep counting.
solid answer
~40 sThe count alone proves nothing — some types honestly need three. The test is which members mention which placeholder. If a registry declares `Key`, `Payload` and `Outcome`, and the routing members mention only the first two while the reply-building members mention only the last, the placeholders have partitioned into disjoint groups and so has the type's purpose. Call sites make the same split visible: routing code is forced to name an `Outcome` it never touches, and reply code names a `Key` it never reads. Splitting into two declarations lets each call site name only what it uses. Naming follows the same growth curve: at one placeholder a single letter is readable, and by the third a reader is matching letters to positions instead of reading roles, so role names stop being a style preference.
code
pseudocode · 14 linestype Route<Key, Payload, Outcome>:
key: Key
decode(raw) -> Payload
encode(o: Outcome) -> raw
// key and decode never mention Outcome; encode never mentions Key or Payload
// routing call sites must still name an Outcome they never produce
type Route<Key, Payload>:
key: Key
decode(raw) -> Payload
type Reply<Outcome>:
encode(o: Outcome) -> rawgo deeper
Carry away the reading habit rather than a rule: when a declaration has several placeholders, check which parts of it actually mention each one before deciding the design is fine.
Be able to run the test — members against placeholders — and to say what a partition implies, including the case where one placeholder belongs on a single operation instead of the whole type.
Diagnose and act. Spot call sites naming type arguments they never use, propose the split along the partition, and defend the declarations where three or four placeholders are simply what the job costs.
Set the convention for published declarations: role names beyond two placeholders, and a partition check before a type grows another one, because splitting a widely used declaration later is a migration every consumer feels.
## The count is a symptom, not the disease "More than two placeholders is a smell" is a rule of thumb, and like most rules of thumb it points at the right place for the wrong reason. Three placeholders are not a defect. What is a defect is a declaration whose placeholders **partition** — where the type is really two types that happen to share a name, and every caller pays for the half it does not use. So the review question is never "how many?" It is: **does every member of this declaration have something to do with every placeholder?** ## The test: which members mention which placeholder Write the members down the page and the placeholders across the top, and mark each cell. Three patterns come out. - **Every member touches every placeholder.** The declaration is coherent; three is honest. Leave it alone. - **Members split into disjoint groups, each using its own placeholders.** The declaration is two things. Split it, and each half loses a placeholder. - **One placeholder is used by exactly one member.** Usually that member wants its own placeholder, declared on the operation rather than on the whole type, so only the calls that use it have to say anything about it. The call sites give you the same answer from the other direction, and it is often quicker to see: - code that only routes is forced to name an outcome type it never produces; - code that only builds replies is forced to name a key type it never reads; - one of the three arguments is written the same way at nearly every call site, because nobody actually varies it. Each of those is a caller paying for a responsibility that belongs to somebody else. ## A worked split A route declaration accumulates a third placeholder for what its handler eventually produces. The routing half — the key and the decoding of an incoming payload — never mentions it, and the reply half never mentions the key. Two declarations follow directly: one describing what a route *is*, one describing what producing a reply *is*. Each call site then names two placeholders at most, and the pairing that genuinely mattered — key with payload — is still stated in one place. Notice what the split did not do: it did not remove a relation, and it did not make anything less typed. It removed a *coupling between unrelated relations*, which is the thing that was costing callers. ## When three is honest Plenty of coherent declarations carry three or more, and refusing them on principle is its own defect. A transformation from one pair to another needs four and every member uses all four. A mapping that reads from one shape, keys by a second and writes a third mentions all three in its central operation. A pipeline stage parameterised over what it takes, what it emits and what it may fail with mentions each in the same signature. In all of these the matrix is full, no caller names something it does not use, and the count is just what the job costs. ## Naming, once the count grows The same growth curve applies to how the placeholders are written. 1. **One placeholder.** A single letter is fine. There is nothing to confuse it with, and the surrounding declaration says what it is. 2. **Two.** A letter pair is usually still readable when the roles are obvious from the type's purpose, but a reader is already matching by position. 3. **Three or more.** Single letters stop carrying their weight entirely. The reader has to hold a mapping from letters to roles in their head while reading a signature that also has value parameters in it, and every explicit call site becomes a positional puzzle. Role names — `Key`, `Payload`, `Outcome` — cost nothing at the point of use and remove that work. They change nothing about what the checker accepts: naming is a message to the reader, not to the tool. What the specific conventions look like differs between ecosystems, and that part is not this subject's to settle; the reader's difficulty is the same everywhere. ## What to say in the interview The compact answer has three moves, and stopping after the first is the common failure: - reject the count as the criterion; - give the test — do the members partition, and does every call site use every placeholder; - name the fix, which is splitting the declaration along the partition, and the case where three is simply correct. A candidate who says only "more than two is a smell" has repeated a slogan. A candidate who asks which members use which placeholder is doing the review.
- Is there a placeholder count above which a declaration is simply wrong?No. A transformation between two parameterised pairs honestly needs four, and every member uses all four. The criterion is coherence, not arithmetic: if some members ignore some placeholders, even two may be one too many, and if every member uses every one, four is what the job costs.
- What is the alternative when only one member needs the extra placeholder?Declare it on that member rather than on the whole type. Then its scope is the single operation, only calls to that operation have anything to say about it, and every other call site and every other member is unaffected — which is also the honest description of what was true all along.
saying these in an interview costs you the question
- Treats the placeholder count as a hard limit instead of checking which members use which
- Believes any third placeholder is automatically a design error
- Thinks role names instead of single letters change what the checker will accept
- Splits a declaration whose every member genuinely mentions all three placeholders
- Assumes single letters stay readable at any count because call sites rarely spell them out