A directory exposes lookup by identifier beside search by name - what does declaring each result's cardinality commit you to?
answer
- the signature carries the count
- one clause every caller reads
- absence is the price of narrow
- declare the domain, not today's data
- widening touches every call site
basics
~20 sDeclaring at most one promises no caller ever sees a second value but obliges every caller to handle absence; declaring many promises nothing about count. The signature makes that decision once, for every caller that ever reads it.
solid answer
~50 sCardinality is the part of a stream contract the signature itself carries, so it is read by every future caller without anyone opening the documentation. Declaring the lookup as **at most one** says two things: nobody will ever receive a second record, and everybody must handle the empty ending. Declaring the search as **many** says the count is unconstrained, so callers must work for zero, one, and n alike, and none of them may assume the whole answer fits in memory. The cost is asymmetric. Declaring too loosely - typing a lookup as many-valued because it is easy - pushes a narrowing decision into every caller forever. Declaring too tightly - typing a search as at-most-one because today it returns a single row - is a promise you break the day a second match appears.
go deeper
Notice that the declared shape of a result tells you how much work you have to do: at most one means no loop but an absence branch, many means you handle zero, one, and lots.
Explain what each declaration promises and obliges, and why declaring loosely pushes a surplus-and-absence decision into every caller while declaring tightly is a promise ordinary data can break.
Show what it costs to change an arity after callers exist, and how you decide whether uniqueness is enforced by the domain or merely observed in the current data.
Set the house rule for stream-returning surfaces: when a narrowed variant is published beside the wide one, how arity changes are staged across consumers, and what reviewers must check before a new operation ships.
## Cardinality is the part of the contract the signature enforces A source's contract has several clauses: what the values mean, what failures can occur, whether work restarts per subscriber, and how many values may arrive. Most of those live in documentation. **Cardinality lives in the declared type**, which means it is the clause every future caller reads whether they want to or not, and the clause a compiler or a reviewer can hold them to. That is why choosing it is a design decision and not a detail of implementation. In a directory service, two operations sit side by side and the choice is already made for you by the domain: | Operation | Honest declared arity | What the caller must handle | What the caller may assume | |---|---|---|---| | Lookup by identifier | at most one | a record, or an empty ending | no second record ever arrives | | Search by name | many | zero, one, or an unbounded number | nothing about the count | ## What the narrower declaration buys, and what it costs Declaring **at most one** buys the caller the right *not* to write a loop, not to think about ordering, not to think about how many values might arrive, and not to think about holding a growing result. In exchange it charges one obligation that cannot be avoided: the empty ending is inside the contract, so absence must be handled. A caller who ignores it produces nothing and reports nothing. Declaring **many** buys flexibility at the cost of obligations everywhere: - every caller must behave correctly for zero values, one value, and a large number; - no caller may assume the result can be collected safely into memory; - any caller that actually wanted a single answer must perform a narrowing, and each one chooses its own policy for surplus and absence. That last point is the real expense. A decision not made in the signature is made again, differently, by every consumer. ## Declaring too loosely The tempting mistake is to declare everything many-valued because it is uniform and never wrong. It is never wrong in the sense that the implementation can always satisfy it - but it throws away information the implementer had and the caller needs. A lookup by identifier that is typed as many-valued invites four separate consequences: 1. Every caller writes its own narrowing, and they disagree about what a second value would mean. 2. Reviewers can no longer tell from the signature whether a second value is possible, so they have to read the implementation. 3. Callers defensively handle a case that cannot occur, which is dead code that still needs testing and maintaining. 4. The genuine invariant - identifiers are unique - stops being expressed anywhere a machine can check. ## Declaring too tightly The opposite mistake is quieter and more expensive. A search by name that happens to return one row today gets typed as at-most-one, and the promise holds right up until a second person shares a name. Then the implementation faces a choice it should never have had: silently drop the extra match, fail, or change the declared arity and touch every caller. The first is data loss disguised as a contract, the second is an outage caused by ordinary data, and the third is the honest option that the original declaration was supposed to avoid. The rule of thumb is to declare the arity the **domain** guarantees, not the arity today's data happens to exhibit. If uniqueness is enforced somewhere - a key, a constraint, an invariant the system maintains - at-most-one is a real promise. If uniqueness is merely observed, it is a coincidence, and the declaration should be many. ## Changing it later is not symmetric - **Widening** at-most-one to many forces every existing caller to handle a multi-value case they were promised could not happen. Every call site is touched. - **Narrowing** many to at-most-one leaves callers holding logic for a case that can no longer arise, and quietly changes the meaning of absence for anyone who was collecting. Neither is free, which is the point: cardinality is the clause of the contract that is hardest to revise, so it deserves the most thought at the time the operation is designed. ## What this looks like in review When reviewing a new stream-returning operation, ask three questions in order. Does the domain guarantee at most one result, or merely produce one today? If many, is the count bounded by anything, and does the caller need to know that? And if the operation is many-valued but nearly every caller narrows it the same way, should the narrowing be part of the published surface rather than repeated at each call site? Those three questions catch most cardinality defects before they become call-site sprawl.
- How do you decide whether at-most-one is a real promise or a coincidence?Ask what enforces it. If uniqueness is maintained by the system - a key, a constraint, an invariant the writer upholds - the promise is real and the narrow declaration is honest. If a single result is merely what today's data produces, it is an observation, and the operation should be declared many-valued.
- Most callers of a many-valued operation narrow it the same way. What does that suggest?That the narrowing belongs on the published surface. Offer the narrowed operation alongside the many-valued one so the surplus-and-absence policy is decided once and named, instead of being reimplemented - and quietly varied - at every call site.
saying these in an interview costs you the question
- Declares everything many-valued because it is uniform and never wrong
- Types an operation from today's data rather than the domain guarantee
- Assumes changing declared arity later is a local edit
- Thinks a narrow declaration removes the caller's obligation to handle absence
- Silently drops extra matches to preserve an at-most-one promise